소스 검색

feat:灵活模板,加入是否继承公共配置,全局扫描配置是否合法等

韩洋 7 달 전
부모
커밋
085a1c6d05
36개의 변경된 파일3013개의 추가작업 그리고 105개의 파일을 삭제
  1. BIN
      src.zip
  2. 1 1
      src/base/ajax.js
  3. 155 4
      src/base/store/use-report-editor.js
  4. 19 3
      src/modules/chat-item/Answer.vue
  5. 1 1
      src/modules/chat-item/DialogOutlineActions.vue
  6. 1 1
      src/modules/chat-item/DialogReportContent.vue
  7. 7 2
      src/modules/chat-item/ReportFile.vue
  8. 5 4
      src/modules/chat-menu/ChatHistory.vue
  9. 80 3
      src/modules/conversation-chat/DialogInput.vue
  10. 1 0
      src/modules/conversation-chat/chatList.vue
  11. 39 0
      src/modules/conversation-chat/dialog-input/InputActions.vue
  12. 110 0
      src/modules/conversation-chat/upload-action/DocxFile.vue
  13. 22 0
      src/modules/conversation-dialog/api-dialog.js
  14. 4 4
      src/modules/conversation-dialog/api-template.js
  15. 14 3
      src/modules/conversation-dialog/format-dialog-answer-from-history.js
  16. 2 0
      src/modules/conversation-dialog/format-dialog-answer-from-sse_receive_message_report.js
  17. 13 0
      src/modules/conversation-dialog/format-dialog-question-by-history.js
  18. 2 1
      src/modules/conversation-dialog/format-html-md-switch-for-report.js
  19. 5 1
      src/modules/conversation-dialog/use-sse-report.js
  20. 47 8
      src/modules/report-template/TemplateDetail.vue
  21. 55 3
      src/modules/report-template/TemplateEditor.vue
  22. 17 5
      src/modules/report-template/editor-model/CommonDimension.vue
  23. 1662 0
      src/modules/report-template/editor-model/EditorDetail copy.vue
  24. 196 13
      src/modules/report-template/editor-model/EditorDetail.vue
  25. 110 12
      src/modules/report-template/editor-model/EditorTitleHeader.vue
  26. 217 0
      src/modules/report-template/editor-model/SaveErrorModal.vue
  27. 13 1
      src/modules/report-template/editor-model/SaveTemplateModel.vue
  28. 86 4
      src/modules/report-template/filter-model/MentionFilter.vue
  29. 1 1
      src/modules/report-template/filter-model/model/DualListSelector.vue
  30. 4 0
      src/modules/report-template/source-model/DataSource.vue
  31. 10 2
      src/modules/report-template/template-detail/EditorModel.vue
  32. 5 0
      src/modules/report-template/template-detail/FilterModel.vue
  33. 1 1
      src/modules/template-manage/template/AddTemplate.vue
  34. 1 1
      src/modules/template-manage/template/TemplateDetailHeader.vue
  35. 105 26
      src/views/conversation/ConversationChatContainer.vue
  36. 2 0
      src/views/report/ReportDetail.vue

BIN
src.zip


+ 1 - 1
src/base/ajax.js

@@ -10,7 +10,7 @@ const ajax = axios.create({
   withCredentials: true, // 跨域发送 cookie
   timeout: 300000, // 请求超时
   headers: {
-    'Content-Type': 'application/json'
+    'Content-Type': 'application/json;charset=utf-8'
   }
 });
 

+ 155 - 4
src/base/store/use-report-editor.js

@@ -221,6 +221,14 @@ export const useReportEditor = defineStore('report-editor', {
       })
     },
 
+    // 配置是否取消继承
+    changeCommonConfigListConfig(common_config_list) {
+      this.metrics_list.forEach(item => {
+        if (item.index === this.target_metric_id) {
+          item.extend_commom_config_info = [...common_config_list.map(it => it)]
+        }
+      })
+    },
     // 配置过滤器
     changeFilterListConfig(filter_list) {
       this.metrics_list.forEach(item => {
@@ -229,10 +237,145 @@ export const useReportEditor = defineStore('report-editor', {
         }
       })
     },
-    //修改is_need_save状态
-    // setNeedSave(value) {
-    //   this.is_need_save = value
-    // },
+    // 检测指标配置是否将所有必填项配置完成,如果配置完成返回空数组,否则返回一个数组,数组中包含需要完善的配置
+    checkMetricConfigComplete() {
+      // 1.获取富文本配置的所有指标 this.metrics_list
+      // 2.遍历指标对应的数据源中所有维度,找到is_request为1的项  this.metrics_list->item.option.source.flexible_template_metric_fields
+      // 3.拿到is_request为1的项的field_key和common_config_type先去 this.metrics_list ->metric_fields中找对应的项
+      // 4.分几种情况,
+      //      1.this.metrics_list中不存在metric_fields,
+      //      2.this.metrics_list中存在metric_fields,但是metric_fields中不存在该field_key,
+      //      3.this.metrics_list中存在metric_fields,metric_fields中存在该field_key,但是该对象中的field_value为空,或者他是一个数组,数组中存在null
+      //      4.如出现上诉情况,则去this.common_config中找对应的common_config_type,如果common_config_type也不存在或者common_config_type对应的值为空或者空字符串,则加入到需要完善的配置数组中,否则检测通过
+
+      // TODO 加入如下逻辑,当判断is_required为1时,则去查看当前指标项中extend_commom_config_info数组是否存在且数组中存在某一项的common_config_type与当前项的common_config_type相同,
+      //      如果对应的is_extend为1,则逻辑与原始一样,需要继承this.common_config对应的配置
+      //      如果对应的is_extend为0,则不去继承this.common_config对应的配置,必须在自己的过滤器metric_fields中配置
+
+      const complete_list = []
+      // 检查metrics_list是否为数组
+      if (Array.isArray(this.metrics_list)) {
+        this.metrics_list.forEach(item => {
+          // 检查item.option和item.option.source是否存在
+          if (item && item.option && item.option.source) {
+            // 检查flexible_template_metric_fields是否存在且为数组
+            const metricFields = item.option.source.flexible_template_metric_fields;
+            if (Array.isArray(metricFields)) {
+              metricFields.forEach(field => {
+                // 检查field是否存在且is_required为1
+                if (field && field.is_required / 1 === 1) {
+                  console.log('field', field)
+                  // 检查metric_fields是否存在且为数组
+                  let need_complete = false;
+
+                  if (!Array.isArray(item.metric_fields)) {
+                    // 情况1: this.metrics_list中不存在metric_fields
+                    need_complete = true;
+                  } else {
+                    // 情况2和3: 检查metric_fields中是否存在该field_key
+                    const metric_field = item.metric_fields.find(it => it && it.field_key === field.field_key);
+
+                    if (!metric_field) {
+                      // 情况2: metric_fields中不存在该field_key
+                      need_complete = true;
+                    } else {
+                      // 情况3: 检查field_value是否为空,或者是数组且包含null
+                      const field_value = metric_field.field_value;
+
+                      if (field_value === null || field_value === undefined || field_value === '') {
+                        // field_value为空
+                        need_complete = true;
+                      } else if (Array.isArray(field_value)) {
+                        // 是数组,检查是否存在null
+                        if (field_value.some(val => val === null || val === undefined || val === '')) {
+                          need_complete = true;
+                        }
+                      }
+                    }
+                  }
+
+                  // 检查extend_commom_config_info
+                  let shouldCheckCommonConfig = true;
+                  if (Array.isArray(item.extend_commom_config_info)) {
+                    const extendConfig = item.extend_commom_config_info.find(
+                      extItem => extItem && extItem.common_config_type === field.common_config_type
+                    );
+                    if (extendConfig && extendConfig.is_extend / 1 === 0) {
+                      // 如果is_extend为0,则不继承common_config,必须在metric_fields中配置
+                      shouldCheckCommonConfig = false;
+                    }
+                  }
+
+                  // 如果上述情况需要完善,则检查common_config
+                  if (need_complete) {
+                    console.log(11111111111)
+                    if (shouldCheckCommonConfig) {
+                      // 检查common_config是否存在且为对象
+                      if (this.common_config && typeof this.common_config === 'object') {
+                        const common_config_value = this.common_config[field.common_config_type];
+
+                        // 检查common_config_type是否存在,且值不为空、不为null、不为空字符串
+                        if (common_config_value === null || common_config_value === undefined || common_config_value === '') {
+                          // common_config_type不存在或者值为空,添加到需要完善的配置数组
+                          complete_list.push({
+                            uid: item.uid,
+                            source_name: item.option.source.metric_name || '',
+                            metric_name: item.option.field_name || '',
+                            filter_name: field.field_name || '',
+                            filter_key: field.field_key || '',
+                            common_config_type: field.common_config_type || '',
+                          });
+                        }
+                        // 否则检测通过,不添加到complete_list
+                      } else {
+                        // common_config不存在或不是对象,添加到需要完善的配置数组
+                        console.log(field, 22222222222)
+                        complete_list.push({
+                          uid: item.uid,
+                          source_name: item.option.source.metric_name || '',
+                          metric_name: item.option.field_name || '',
+                          filter_name: field.field_name || '',
+                          filter_key: field.field_key || '',
+                          common_config_type: field.common_config_type || '',
+                        });
+                      }
+                    } else {
+                      // 如果不继承common_config,则直接添加到需要完善的配置数组
+                      complete_list.push({
+                        uid: item.uid,
+                        source_name: item.option.source.metric_name || '',
+                        metric_name: item.option.field_name || '',
+                        filter_name: field.field_name || '',
+                        filter_key: field.field_key || '',
+                        common_config_type: field.common_config_type || '',
+                      });
+                    }
+                  }
+                }
+              });
+            }
+          }
+        });
+      }
+      // 返回结果数组
+      return complete_list;
+    },
+    // 清空store
+    clearAll() {
+      this.metrics_list = [] // 编辑器中添加到指标数据
+      this.common_config = {
+        common_start_time: '', // 公共维度-时间范围-开始时间
+        common_end_time: '', // 公共维度-时间范围-结束时间
+        common_area: '', // 公共维度-区域
+      } // 公共维度配置放在这里
+      this.existing_match_list = []// 存在的度量数据,用于匹配富文本中的指标
+      this.target_metric_id = null // 当前选中的指标数据
+      this.target_report_markdown = ''// 当前富文本中内容md格式
+      this.target_template_id = '' // 当前编辑的模板id
+      this.is_need_save = false // 判断富文本及过滤器中是否有指标要修改,若有则为true,否则为false
+      this.is_need_save_template = false // 上同,区分为模板
+    }
+
 
   },
 
@@ -253,6 +396,14 @@ export const useReportEditor = defineStore('report-editor', {
     QueryTargetFilterListConfig: (state) => {
       return state.metrics_list.find(item => item.index === state.target_metric_id)?.metric_fields || []
     },
+    // 获取当前指标中配置的是否取消继承
+    QueryTargetExtendCommonConfigInfo: (state) => {
+      return state.metrics_list.find(item => item.index === state.target_metric_id)?.extend_commom_config_info || []
+    },
+    // 获取当前指标中配置的是否取消继承
+    HasTargetExtendCommonConfigInfo: (state) => {
+      return state.metrics_list.find(item => item.index === state.target_metric_id)?.extend_commom_config_info || null
+    },
 
     // 获取当前富文本的md
     getTargetMd: (state) => {

+ 19 - 3
src/modules/chat-item/Answer.vue

@@ -458,10 +458,16 @@ const handleUpdate = (icon) => {
 
 const handleReportEdit = () => {
   // console.log(props.dialog, 888);
-  if (props.dialog.report_files.record_id) {
+  if (props.dialog?.report_files?.record_id) {
     router.push({
       path: `/report/${props.dialog.report_files.record_id}`
     });
+  } else {
+    if (props.dialog?.record_id) {
+      router.push({
+        path: `/report/${props.dialog.record_id}`
+      });
+    }
   }
 };
 const handleSelect = (v) => {
@@ -514,7 +520,7 @@ const downloadMergeExcel = async () => {
 
 const toDownload = () => {
   const record_id = props.dialog.report_files.record_id;
-  console.log(record_id, 11111111111111111);
+
   if (record_id) {
     is_download_loading.value = true;
     downloadReport({ record_id })
@@ -556,10 +562,20 @@ const toDownloadExcel = () => {
 };
 const toUpdateOutLine = () => {
   let md_content = '';
+  console.log(props.dialog);
+  console.log(props.dialog.result_list);
+
   if (props.dialog?.result_list && props.dialog?.result_list[0]) {
     md_content = props.dialog?.result_list[0];
   }
-  emit('update_outline', md_content);
+  const data = {
+    md_content,
+    start_date: props.dialog.client_custom_report_outline_start_date || '',
+    end_date: props.dialog.client_custom_report_outline_end_date || '',
+    area: props.dialog.client_custom_report_outline_area || '',
+    theme: props.dialog.client_custom_report_outline_theme || ''
+  };
+  emit('update_outline', data);
 };
 
 const toConfirmOutLine = () => {

+ 1 - 1
src/modules/chat-item/DialogOutlineActions.vue

@@ -5,7 +5,7 @@
       <div classs="tips-text">{{ tips }}</div>
       <div class="tools">
         <a-space>
-          <a-button type="primary" @click="toUpdate">确认</a-button>
+          <a-button type="primary" @click="toUpdate">编辑</a-button>
           <!-- <a-button type="primary" @click="handleConfirm">确认</a-button> -->
         </a-space>
       </div>

+ 1 - 1
src/modules/chat-item/DialogReportContent.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="dialog-chat-answer-content">
-    <div v-if="is_loading && type == 'bgsc'" class="loading"><a-spin></a-spin> <span>报告生成中...</span></div>
+    <div v-if="is_loading && type == 'bgsc'" class="loading"><a-spin></a-spin> <span>内容生成中...</span></div>
     <div v-if="is_loading && type == 'znws'" class="loading"><a-spin></a-spin> <span>正在思考中...</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>

+ 7 - 2
src/modules/chat-item/ReportFile.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="report-file">
-    <img class="icon" :src="DOCX" alt="" />
+    <img class="icon" :src="getFileType(file.title) == 'docx' ? DOCX : Excel" alt="" />
     <div class="info">
       <a-tooltip :content="file.title">
         <div class="title">{{ ellipsisMiddle(file.title) }}</div>
@@ -15,7 +15,8 @@
 
 <script setup>
 import { ref, computed } from 'vue';
-import DOCX from '../../assets/chat-report/excel-icon.svg';
+import Excel from '../../assets/chat-report/excel-icon.svg';
+import DOCX from '../../assets/chat-report/word-icon.svg';
 
 const props = defineProps({
   file: Object
@@ -34,6 +35,10 @@ function ellipsisMiddle(str) {
 
   return str.slice(0, leftLength) + '...' + str.slice(-rightLength);
 }
+
+function getFileType(title) {
+  return title.split('.').at(-1);
+}
 const remove = (file) => {
   // 正在上传文件不允许删除
   if (file.status / 1 === 1) return;

+ 5 - 4
src/modules/chat-menu/ChatHistory.vue

@@ -177,7 +177,7 @@
     v-if="showEdit"
     :visible="showEdit"
     :editItemData="editItemData"
-    :is_loading="is_loading"
+    :is_loading="is_loading_edit"
     @ok="handleEditOk"
     @cancel="handleEditCancel"
   >
@@ -194,6 +194,7 @@ import { deleteHistoryItem, updateHistoryItem } from '../conversation-dialog/api
 import DeleteModal from '../../components/DeleteModal.vue';
 import HistoryEditModal from './HistoryEditModal.vue';
 import Empty from './Empty.vue';
+import { Message } from '@arco-design/web-vue';
 
 const props = defineProps({
   is_electron: Boolean,
@@ -213,7 +214,7 @@ const showDelete = ref(false);
 const delete_id = ref('');
 const showEdit = ref(false);
 const editItemData = ref({});
-const is_loading = ref(false);
+const is_loading_edit = ref(false);
 // const history_list = ref([
 //   {
 //     id: 1,
@@ -365,7 +366,7 @@ const handleEditCancel = () => {
   editItemData.value = {};
 };
 const handleEditOk = async (item) => {
-  is_loading.value = true;
+  is_loading_edit.value = true;
   const res = await updateHistoryItem({ id: item.id, title: item.title });
   if (res.code / 1 === 200) {
     emit('updateHistoryItem', item);
@@ -373,7 +374,7 @@ const handleEditOk = async (item) => {
     showEdit.value = false;
     editItemData.value = {};
   }
-  is_loading.value = false;
+  is_loading_edit.value = false;
 };
 const handleDeleteOk = async () => {
   const res = await deleteHistoryItem({ id: delete_id.value });

+ 80 - 3
src/modules/conversation-chat/DialogInput.vue

@@ -5,6 +5,11 @@
       <slot name="top">
         <UploadList v-if="upload_file_list.length" :upload_file_list="upload_file_list" @removeFile="remove">
         </UploadList>
+        <DocxFile
+          v-if="upload_file_for_outline.title"
+          :file="upload_file_for_outline"
+          @removeFile="removeFileForOutLine"
+        ></DocxFile>
       </slot>
 
       <div class="textarea-container" v-if="has_textarea">
@@ -56,9 +61,11 @@
         :chat_type="chat_type"
         :bgcl_btn_status="bgcl_btn_status"
         :is_merge_table="is_merge_table"
+        :display_upload_for_ouline="show_upload_for_ouline"
         @submit="handleSubmit"
         @stop="handleStop"
         @upload_files="uploadFiles"
+        @upload_file_For_Outline="uploadFileForOutline"
         @recommend="toRecommend"
         @open_template_select="openTemplateSelect"
         @open_draft="openDraft"
@@ -88,6 +95,7 @@
 import { ref, onMounted, onUnmounted, watch, computed, nextTick } from 'vue';
 import InputActions from './dialog-input/InputActions.vue';
 import UploadList from './upload-action/UploadList.vue';
+import DocxFile from './upload-action/DocxFile.vue';
 import TextAreaTemplate from './dialog-input/TextAreaTemplate.vue';
 import TextAreaTemplateSelected from './dialog-input/TextAreaTemplateSelected.vue';
 
@@ -125,7 +133,14 @@ const props = defineProps({
   }
 });
 
-const emit = defineEmits(['submit', 'sizeChange', 'actionConfigChange', 'changeFooterStyle', 'open_template_manage']);
+const emit = defineEmits([
+  'submit',
+  'sizeChange',
+  'actionConfigChange',
+  'changeFooterStyle',
+  'open_template_manage',
+  'changeFooterStyleForOutline'
+]);
 
 /**
  * 文本域输入的内容
@@ -141,6 +156,8 @@ const upload_file_list = ref([]);
 const upload_queue = ref([]);
 // 是否正在上传
 const isUploading = ref(false);
+// 文件模板上传
+const upload_file_for_outline = ref({});
 // 默认显示的输入框是模板输入框
 const is_template_input = ref(true);
 // 是否显示模板输入框
@@ -151,6 +168,8 @@ const show_template_select_model = ref(false);
 const show_draft_model = ref(false);
 // 选中的模板
 const selected_template = ref(null);
+// 是否显示上传模板按钮
+const show_upload_for_ouline = ref(true);
 
 /**
  * 表格处理,文件上传,基准文件id
@@ -212,6 +231,9 @@ const handleSubmit = () => {
     selected_template.value = null;
     return;
   }
+  if (props.chat_type === 'bgsc' && upload_file_for_outline.value.title) {
+    input_file_list.value = [upload_file_for_outline.value];
+  }
 
   emit('submit', {
     text: input_text_value.value,
@@ -262,6 +284,15 @@ const handleSubmitSelectedTemplate = (text) => {
     is_template_input.value = false;
   }
 };
+
+const uploadFileForOutline = (file) => {
+  upload_file_for_outline.value = {
+    title: file.name,
+    size: getFileSize(file.size),
+    origin_file: file
+  };
+};
+
 const uploadFiles = (files) => {
   let file_list = files.map((item) => {
     return {
@@ -357,6 +388,9 @@ const remove = (key) => {
   upload_file_list.value = upload_file_list.value.filter((item) => item.key !== key);
   upload_queue.value = upload_file_list.value.filter((item) => item.key !== key);
 };
+const removeFileForOutLine = () => {
+  upload_file_for_outline.value = {};
+};
 
 const clear = () => {
   event_id.value = '';
@@ -365,6 +399,8 @@ const clear = () => {
   upload_file_list.value = [];
   upload_queue.value = [];
   selected_template.value = null;
+  show_upload_for_ouline.value = true;
+  upload_file_for_outline.value = {};
 };
 const toRecommend = (txt) => {
   input_text_value.value = txt;
@@ -464,8 +500,12 @@ const handleTextKeyUp = (e) => {
   }
 };
 const initInput = () => {
-  is_template_input.value = true;
-  show_template_input.value = false;
+  is_template_input.value = false;
+  show_template_input.value = true;
+  nextTick(() => {
+    is_template_input.value = true;
+    show_template_input.value = false;
+  });
 };
 
 watch(
@@ -482,6 +522,43 @@ watch(
   }
 );
 
+watch(
+  () => upload_file_for_outline.value,
+  () => {
+    if (upload_file_for_outline.value?.title) {
+      emit('changeFooterStyle', true);
+      is_template_input.value = false;
+      show_template_input.value = false;
+      input_text_value.value = '根据上传的文档模板,为你生成匹配格式与内容要求的大纲';
+    } else {
+      emit('changeFooterStyle', false);
+      // is_template_input.value = true;
+      // show_template_input.value = false;
+      input_text_value.value = '';
+      // is_footer_style.value = false;
+    }
+  },
+  {
+    deep: true
+  }
+);
+
+watch(
+  () => selected_template.value,
+  () => {
+    // 当选中模板时,不允许上传
+    if (selected_template.value?.id) {
+      upload_file_for_outline.value = {};
+      show_upload_for_ouline.value = false;
+    } else {
+      show_upload_for_ouline.value = true;
+    }
+  },
+  {
+    deep: true
+  }
+);
+
 onMounted(() => {
   // 对话输入框自动获得焦点
   autoFocus();

+ 1 - 0
src/modules/conversation-chat/chatList.vue

@@ -39,6 +39,7 @@ const toMerge = (data) => {
   emit('merge', data);
 };
 const toUpdateOutLine = (data) => {
+  console.log(data, 111);
   emit('update_outline', data);
 };
 const toConfirmOutLine = (data) => {

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

@@ -27,6 +27,17 @@
           <img class="icon_upload" src="../../../assets/chat-input/upload_pic.svg" alt="" />
         </a-tooltip> -->
       </div>
+      <div class="upload-btns" v-if="show_upload_for_ouline">
+        <a-tooltip content="支持上传文件(接受docx类型)">
+          <img
+            class="icon_upload"
+            :class="is_merge_table ? 'icon_upload_disabled' : ''"
+            src="../../../assets/chat-input/upload_file.svg"
+            alt=""
+            @click="toUploadFileByOutline"
+          />
+        </a-tooltip>
+      </div>
       <a-tooltip :content="tip_content" position="left">
         <img v-if="btn_status == 0" class="action_btn_disabled" :src="submit_disabled" alt="禁用" />
         <img v-if="btn_status == 1" class="action_btn_normal" @click="handleSubmit" :src="submit_normal" alt="发送" />
@@ -35,6 +46,7 @@
     </div>
   </div>
   <input ref="fileRef" type="file" accept=".xlsx, .xls" multiple style="display: none" @change="handleFileChange" />
+  <input ref="fileOutlineRef" type="file" accept=".docx" style="display: none" @change="handleFileByOutlineChange" />
   <RecommendModal v-if="show_recommend_tips" class="recommend-modal" @recommend="toRecommend" @close="toCloseRecommend">
   </RecommendModal>
 </template>
@@ -150,6 +162,10 @@ const props = defineProps({
   templateData: {
     type: Array,
     default: () => []
+  },
+  display_upload_for_ouline: {
+    type: Boolean,
+    default: true
   }
 });
 
@@ -160,6 +176,7 @@ const emit = defineEmits([
   'submit',
   'actionConfig',
   'upload_files',
+  'upload_file_For_Outline',
   'changeRecommendStatus',
   'open_template_select',
   'open_draft'
@@ -170,6 +187,7 @@ const btn_auth = ref();
 const tip_content = ref('请输入你的问题');
 
 const fileRef = ref(null);
+const fileOutlineRef = ref(null);
 const show_recommend_tips = ref(false);
 
 const limit_excel_types = [
@@ -177,6 +195,8 @@ const limit_excel_types = [
   'application/vnd.ms-excel'
 ];
 
+const limit_docx_types = ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
+
 const submit_normal = computed(() => {
   if (themeStore.value == THEME_GREEN) {
     return submit_normal_a;
@@ -228,6 +248,9 @@ const has_recommend = computed(() => {
 const show_upload = computed(() => {
   return props.chat_type === 'bgcl';
 });
+const show_upload_for_ouline = computed(() => {
+  return props.chat_type === 'bgsc' && props.display_upload_for_ouline && window.show_outline_upload_exe;
+});
 const has_template = computed(() => {
   return props.chat_type === 'bgsc';
   // return props.chat_type === 'bgcs' && false;
@@ -281,6 +304,10 @@ const toUploadFiles = () => {
   fileRef.value.click();
 };
 
+const toUploadFileByOutline = () => {
+  fileOutlineRef.value.click();
+};
+
 const handleFileChange = (e) => {
   // console.log(e.target.files);
   const file_list = Array.from(e.target.files);
@@ -293,6 +320,18 @@ const handleFileChange = (e) => {
   }
   fileRef.value.value = '';
 };
+
+const handleFileByOutlineChange = (e) => {
+  console.log(e);
+  const file_list = Array.from(e.target.files);
+  let is_valid = file_list.every((item) => limit_docx_types.includes(item.type));
+  if (!is_valid) {
+    return Message.error('上传文件不合法!');
+  } else {
+    emit('upload_file_For_Outline', file_list[0]);
+  }
+  fileOutlineRef.value.value = '';
+};
 //打开推荐问题弹窗
 const handleRecommend = () => {
   // emit('changeRecommendStatus')

+ 110 - 0
src/modules/conversation-chat/upload-action/DocxFile.vue

@@ -0,0 +1,110 @@
+<template>
+  <div class="upload-list">
+    <div class="report-file">
+      <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.size }}</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>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed } from 'vue';
+import DOCX from '../../../assets/chat-report/word-icon.svg';
+
+const props = defineProps({
+  file: Object
+});
+
+const emit = defineEmits(['toDownload', 'removeFile']);
+
+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">
+.upload-list {
+  position: relative;
+  padding: 0px 20px 10px 20px;
+  display: flex;
+  /* 父容器需给滚动容器留出空间,这里去掉 gap,交给 scroll-container 内部处理 */
+  /* gap: 20px;  */
+}
+.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;
+}
+
+.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;
+}
+</style>

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

@@ -543,3 +543,25 @@ export const toEvaluation = (id, status) => {
 }
 
 
+// 报告大纲模板上传
+export const uploadForOutline = (data) => {
+  return new Promise((resolve, reject) => {
+    ajax
+      .post(
+        `/iaserverapi/v1/report/app/conversation/upload_word`,
+        data,
+        {
+          headers: {
+            "Content-Type": 'multipart/form-data'
+          },
+
+        }
+      )
+      .then((res) => {
+        resolve({ code: res.code, data: res.data, msg: res.msg || '' });
+      })
+      .catch((err) => {
+        reject(err);
+      });
+  });
+}

+ 4 - 4
src/modules/conversation-dialog/api-template.js

@@ -81,7 +81,7 @@ export const addTemplate = async ({ group_id, name, remark, content, template_co
         }
       }
     ).then((res) => {
-      resolve({ code: res.code, data: res.data });
+      resolve({ code: res.code, data: res.data, msg: res.msg || '' });
     })
       .catch((err) => {
         reject(err);
@@ -103,7 +103,7 @@ export const addTemplateGroup = async ({ name, remark }) => {
         }
       }
     ).then((res) => {
-      resolve({ code: res.code, data: res.data });
+      resolve({ code: res.code, data: res.data, msg: res.msg || '' });
     })
       .catch((err) => {
         reject(err);
@@ -125,7 +125,7 @@ export const editTemplateGroup = async ({ id, name, remark }) => {
         }
       }
     ).then((res) => {
-      resolve({ code: res.code, data: res.data });
+      resolve({ code: res.code, data: res.data, msg: res.msg || '' });
     })
       .catch((err) => {
         reject(err);
@@ -198,7 +198,7 @@ export const editTemplateItem = async ({ id, name, remark, status, content, temp
         }
       }
     ).then((res) => {
-      resolve({ code: res.code, data: res.data });
+      resolve({ code: res.code, data: res.data, msg: res.msg || '' });
     })
       .catch((err) => {
         reject(err);

+ 14 - 3
src/modules/conversation-dialog/format-dialog-answer-from-history.js

@@ -197,7 +197,17 @@ export default function formatDialogAnswerFromHistory(item, type, fileLength = 0
     } catch (e) {
       result_list[0] = item.answer
     }
-
+    if (item.references && !Array.isArray(item.references)) {
+
+      if (item.references?.report_type / 1 === 3) {
+        history_item.report_type = item.references.report_type
+        history_item.client_custom_report_outline = true
+        history_item.client_custom_report_outline_start_date = item.references.date?.start_date || ''
+        history_item.client_custom_report_outline_end_date = item.references.date?.end_date || ''
+        history_item.client_custom_report_outline_area = item.references?.area || '';
+        history_item.client_custom_report_outline_theme = item.references?.theme || '';
+      }
+    }
     // console.log(result_list, 2222)
 
     if (Array.isArray(result_list)) {
@@ -220,8 +230,9 @@ export default function formatDialogAnswerFromHistory(item, type, fileLength = 0
       history_item.result_list = result_list.map(item => item.data || item)
       console.log(history_item, 1111)
     }
-    if (result_list[0] !== '请给出日期范围!') {
+    if ((result_list[0] !== '请给出日期范围!' || result_list[0] !== '用户意图分析失败') && (history_item.report_type / 1 !== 3)) {
       history_item.has_echarts = true
+      history_item.record_id = item.id
       // 等于2是月报,其余的是周报,月报不存在表格,周报二者皆有
       // if (item.request_id / 1 !== 2 && item.request_id / 1 !== 3) {
       console.log(item.request_id, 9099)
@@ -254,7 +265,7 @@ export default function formatDialogAnswerFromHistory(item, type, fileLength = 0
         }
       }
 
-      if (item.references && item.references.length) {
+      if (item.references && item.references.length && item.request_id !== "create_outline_by_word") {
         history_item.client_custom_attach_list = item.references
       }
     }

+ 2 - 0
src/modules/conversation-dialog/format-dialog-answer-from-sse_receive_message_report.js

@@ -151,6 +151,8 @@ export default function formatDialogAnswerFromSseReceiveMessageReport(message_ob
       currentMsg.client_custom_attach_list = res_attachments
     }
   }
+
+
   if (is_end) {
     chat_status = 2
     currentMsg.client_custom_chat_status = 2;

+ 13 - 0
src/modules/conversation-dialog/format-dialog-question-by-history.js

@@ -4,9 +4,13 @@ export default function formatDialogQuestionByHistory(item, type) {
   let files = null
   let answer = item.answer
   let client_custom_item_type = 'q'
+  console.log(item, 222)
   if (item.files) {
     files = item.files
   }
+  if (item.references && item.references.length) {
+    files = item.references
+  }
   if (item.query && item.query.files) {
     files = item.query.files
   }
@@ -17,6 +21,15 @@ export default function formatDialogQuestionByHistory(item, type) {
       }
     })
   }
+  if (type / 1 == 2 && files) {
+    console.log(files, 333)
+    files = files.map(it => {
+      return {
+        title: it.file_name,
+      }
+    })
+
+  }
   if (type / 1 == 4) {
     const question_obj = JSON.parse(item.answer)
     console.log(question_obj, 678)

+ 2 - 1
src/modules/conversation-dialog/format-html-md-switch-for-report.js

@@ -43,11 +43,12 @@ export const formatMdToHtml = (exist_metrics, md) => {
    * */
   // 只有当抽取出的指标extract_metrics中的metric_name名称在exist_metrics中存在存在才去转化成成span,否则不转换
 
-
+  if (!md) return '';
   // 创建存在的指标名称集合,用于快速查找
   const existMetricNames = new Set(exist_metrics.map(metric => metric.field_name));
   console.log(existMetricNames, 999)
 
+
   // 先解析markdown为html
   let html = marked.parse(md);
 

+ 5 - 1
src/modules/conversation-dialog/use-sse-report.js

@@ -75,7 +75,8 @@ export default function useFetchEventSourceReport() {
       query,
       stream,
       user_id,
-      template_id
+      template_id,
+      word_file_id
 
     } = queryParams;
 
@@ -105,6 +106,9 @@ export default function useFetchEventSourceReport() {
       // end_date,
 
     };
+    if (word_file_id) {
+      params.word_file_id = word_file_id
+    }
 
     const sse_url = getSseUrl();
     if (!sse_url) {

+ 47 - 8
src/modules/report-template/TemplateDetail.vue

@@ -13,6 +13,7 @@
         :draft_id="draft_id"
         :template_save_time="template_save_time"
         :is_loading_template_detail="is_loading_template_detail || is_metrics_loading"
+        :common_config="common_config"
         @change="handleChange"
         @mention-click="handleMentionClick"
         @close-filter-model="closeFilterModel"
@@ -21,11 +22,13 @@
         @save-template="handleSaveTemplate"
         @content-changed="handleContentChanged"
         @update-draft="handleUpdateDraft"
+        @send-report="handleSendReport"
       ></EditorModel>
       <FilterModel
         v-if="show_filter_model"
         ref="filterModelRef"
         :filter_option="target_filter_option"
+        :target_common_config_info="target_common_config_info"
         @content-changed="handleContentChanged"
       ></FilterModel>
     </div>
@@ -84,10 +87,11 @@ const props = defineProps({
   is_loading_template_detail: {
     type: Boolean,
     default: false
-  }
+  },
+  common_config: Object
 });
 
-const emit = defineEmits(['save-template', 'update-draft']);
+const emit = defineEmits(['save-template', 'update-draft', 'send-report']);
 
 const show_filter_model = ref(false);
 const target_filter_option = ref(null);
@@ -97,6 +101,8 @@ const metrics_list = ref([]); // 度量列表
 const dimensions_list = ref([]); // 维度列表
 const target_source_id = ref('');
 
+const target_common_config_info = ref([]);
+
 const extracted_metrics = ref([]);
 
 const render_html = ref('');
@@ -124,8 +130,10 @@ const handleChange = () => {
 };
 
 const handleMentionClick = (option) => {
-  console.log(option, 890);
+  // console.log(option, 890);
+  // console.log(target_source_id.value);
   target_source_id.value = option.option.source.id || '';
+  // console.log(target_source_id.value);
   target_filter_option.value = null;
   nextTick(() => {
     target_filter_option.value = option;
@@ -133,7 +141,7 @@ const handleMentionClick = (option) => {
   });
 };
 const handleMetricsClick = (item) => {
-  console.log(item);
+  // console.log(item);
   // target_filter_option.value = item;
   if (
     target_filter_option.value &&
@@ -157,10 +165,12 @@ const getMetricsFun = async () => {
     if (res.code / 1 === 200) {
       data_source_list.value = res.data;
       target_source_id.value = data_source_list.value[0]?.id;
+      // console.log(target_source_id.value, 890);
       if (data_source_list.value.length > 0) {
         const flexible_template_metric_fields = data_source_list.value[0].flexible_template_metric_fields;
         if (flexible_template_metric_fields.length) {
           setDataToMetricsAndDimension(flexible_template_metric_fields, data_source_list.value[0]);
+          getTargetCommonConfigInfo(flexible_template_metric_fields);
         }
       }
       // 获取所有数据源中的度量数据
@@ -184,6 +194,21 @@ const setDataToMetricsAndDimension = (flexible_template_metric_fields, orgin_sou
   }));
   dimensions_list.value = flexible_template_metric_fields.filter((item) => item.column_role === 'dim');
 };
+
+const getTargetCommonConfigInfo = (flexible_template_metric_fields) => {
+  target_common_config_info.value = [];
+  flexible_template_metric_fields.forEach((item) => {
+    if (item.column_role === 'dim' && item.common_config_type) {
+      const config = {
+        field_key: item.field_key,
+        field_name: item.field_name,
+        is_extend: 1,
+        common_config_type: item.common_config_type
+      };
+      target_common_config_info.value.push(config);
+    }
+  });
+};
 const extractMeasMetrics = (data) => {
   return data
     .flatMap((item) => {
@@ -204,7 +229,9 @@ const handleToSaveSetting = () => {
 const handleChangeSource = (id) => {
   data_source_list.value.forEach((item) => {
     if (item.id === id) {
+      target_source_id.value = id;
       setDataToMetricsAndDimension(item.flexible_template_metric_fields, item);
+      getTargetCommonConfigInfo(item.flexible_template_metric_fields);
     }
   });
   show_filter_model.value = false;
@@ -213,6 +240,14 @@ const handleSubmitHtml = (html) => {
   submit_html.value = html;
   const md = formatHtmlToMd(submit_html.value);
   console.log(md);
+  // 检查是否包含非ASCII字符
+  const hasNonAscii = /[^\x00-\x7F]/.test(md);
+  // console.log('包含非ASCII字符:', hasNonAscii);
+
+  // UTF-8编码验证
+  const encoder = new TextEncoder();
+  const utf8Bytes = encoder.encode(md);
+  // console.log('UTF-8字节数:', utf8Bytes.length);
   reportEditor.setTargetReportMarkdown(md);
 };
 const handleSaveTemplate = (data) => {
@@ -220,7 +255,7 @@ const handleSaveTemplate = (data) => {
 };
 
 const handleContentChanged = funcDebounce(() => {
-  console.log('需要各项');
+  console.log('需要保存');
   if (props.template_id) {
     reportEditor.setIsNeedSaveTemplate(true);
   } else {
@@ -242,11 +277,11 @@ const isMetricsLoaded = ref(false);
 const performRender = () => {
   const existing_match_list = reportEditor.QueryExistingMatchList;
   render_html.value = formatMdToHtml(existing_match_list, props.md_data);
-  console.log(props.md_data, 99999999);
-  console.log(existing_match_list, render_html.value, 99999999999);
+  // console.log(props.md_data, 99999999);
+  // console.log(existing_match_list, render_html.value, 99999999999);
   const extract_metrics = getExistMetrics(existing_match_list, props.md_data);
   extracted_metrics.value = extract_metrics || [];
-  console.log('xxxxx', extracted_metrics.value);
+  // console.log('xxxxx', extracted_metrics.value);
   reportEditor.addMetric(extract_metrics);
 };
 
@@ -258,6 +293,10 @@ const checkRenderReady = () => {
   }
 };
 
+const handleSendReport = (data) => {
+  emit('send-report', data);
+};
+
 // 监听模板详情加载状态变化
 watch(
   () => props.is_loading_template_detail,

+ 55 - 3
src/modules/report-template/TemplateEditor.vue

@@ -9,12 +9,19 @@
         :draft_id="draft_id"
         :template_save_time="template_save_time"
         :is_loading_template_detail="is_loading_template_detail"
+        :common_config="target_common_config"
         @save-template="handleSaveTemplate"
         @update-draft="handleUpdateDraft"
+        @send-report="handleSendReport"
       ></TemplateDetail>
     </div>
   </a-modal>
   <TipsModel v-model:visible="showTips" :loading="is_loading_edit" @ok="toSaveSetting" @cancel="toNotSaveSetting" />
+  <SaveErrorModal
+    v-model:visible="show_save_error"
+    :complete_list="save_error_list"
+    @ok="handleSaveErrorOk"
+  ></SaveErrorModal>
 </template>
 
 <script setup lang="js">
@@ -26,6 +33,7 @@ import {checkTemplateDetail,editTemplateItem} from '../conversation-dialog/api-t
 import { useReportEditor } from '../../base/store/use-report-editor';
 import {formatDateTime} from '../../utils/utils.js'
 import TipsModel  from './editor-model/TipsModel.vue'
+import SaveErrorModal from './editor-model/SaveErrorModal.vue'
 import { Message } from '@arco-design/web-vue';
 
 const reportEditor = useReportEditor();
@@ -38,8 +46,20 @@ const props = defineProps({
   target_outline_md:{
     type: String,
     default:''
-  }
+  },
+  target_common_config:{
+    type: Object,
+    default:()=>{
+      return {
+        start_date:'',
+        end_date:'',
+        theme:'',
+        area:''
+      }
+    }
+  },
 })
+
 const template_content = ref(props.target_outline_md||'')
 const template_config = ref([])
 const common_config = ref({})
@@ -51,8 +71,12 @@ const template_save_time = ref('')
 const is_loading_template_detail = ref(false)
 const showTips = ref(false)
 const is_loading_edit = ref(false)
+const save_error_list = ref([])
+const show_save_error = ref(false)
+
+
 
-const emit = defineEmits(['toBackHome']);
+const emit = defineEmits(['toBackHome','send-report']);
 
 const visible = computed(()=>{
   return true
@@ -114,10 +138,18 @@ const toBack = () => {
     showTips.value = true
   } else {
     emit('toBackHome');
+    reportEditor.clearAll()
   }
   // emit('toBackHome');
 }
 const toSaveSetting = async () => {
+   const complete_list = reportEditor.checkMetricConfigComplete();
+    if (complete_list.length > 0) {
+      // Message.error('指标配置不完善,请完善后再保存');
+      save_error_list.value = complete_list;
+      show_save_error.value = true;
+      return;
+    }
   const res = await editTemplateFunc(template_id.value)
   if(res.code/1 === 200) {
     Message.success('保存成功')
@@ -128,6 +160,12 @@ const toSaveSetting = async () => {
   }
 }
 
+const handleSaveErrorOk = () => {
+  show_save_error.value = false
+  save_error_list.value = []
+  showTips.value = true
+}
+
 const toNotSaveSetting = () => {
   showTips.value = false
   reportEditor.setIsNeedSaveTemplate(false)
@@ -154,13 +192,27 @@ const editTemplateFunc = async (id) => {
     is_loading_edit.value = false
   }
 };
+
+const handleSendReport = (data) => {
+  emit('send-report', data);
+  reportEditor.clearAll()
+};
 watch(()=>props.target_template_id, () => {
   if(props.target_template_id) {
     getTemplateDetail();
   }
 }, {immediate: true})
 
-
+watch(()=>props.target_common_config,() => {
+  // console.log(props.target_common_config)
+  reportEditor.setCommonConfig('common_start_time', props.target_common_config?.start_date);
+  reportEditor.setCommonConfig('common_end_time', props.target_common_config?.end_date);
+  reportEditor.setCommonConfig('common_area', props.target_common_config?.area);
+    // console.log(props.target_common_config,reportEditor)
+},{
+  immediate:true,
+  deep:true
+})
 
 </script>
 <style scoped lang="css">

+ 17 - 5
src/modules/report-template/editor-model/CommonDimension.vue

@@ -4,12 +4,11 @@
       <div class="item">
         <span>时间范围:</span>
         <a-range-picker
-          showTime
           style="width: 380px"
-          :time-picker-props="{ defaultValue: '00:00:00' }"
-          format="YYYY-MM-DD HH:mm:ss"
+          format="YYYY-MM-DD"
           v-model="time_range"
           @change="handleTimeRangeChange"
+          @clear="handleClearTimeRange"
         >
           <template #suffix-icon> <icon-caret-down class="item-end-icon" /> </template>
           <template #prefix>
@@ -63,6 +62,10 @@ import { getRegionEnum } from '../../../modules/conversation-dialog/api-template
 import { useReportEditor } from '../../../base/store/use-report-editor.js';
 
 const reportEditor = useReportEditor();
+
+const props = defineProps({
+  common_config: Object
+});
 const emit = defineEmits(['content-changed']);
 
 // const common_start_time = ref('');
@@ -83,13 +86,20 @@ const getRegionEnumFun = async () => {
 };
 const handleTimeRangeChange = (newVal, oldVal) => {
   console.log(newVal, oldVal, 999);
-  if (newVal.length === 2) {
+  if (newVal?.length === 2) {
     reportEditor.setCommonConfig('common_start_time', newVal[0]);
     reportEditor.setCommonConfig('common_end_time', newVal[1]);
   }
   emit('content-changed');
 };
 
+const handleClearTimeRange = () => {
+  time_range.value = [];
+  reportEditor.setCommonConfig('common_start_time', '');
+  reportEditor.setCommonConfig('common_end_time', '');
+  emit('content-changed');
+};
+
 const handleAreaChange = (newVal) => {
   console.log(newVal, 999);
   if (newVal) {
@@ -111,7 +121,8 @@ watch(
     }
   },
   {
-    deep: true
+    deep: true,
+    immediate: true
   }
 );
 
@@ -129,6 +140,7 @@ onMounted(() => {
   padding: 0px 10px;
   width: 100%;
   overflow: hidden;
+  box-sizing: border-box;
 }
 .common-dimension-items {
   display: flex;

+ 1662 - 0
src/modules/report-template/editor-model/EditorDetail copy.vue

@@ -0,0 +1,1662 @@
+<template>
+  <div class="rich-text-editor" ref="containerRef">
+    <!-- 工具栏:左侧格式按钮 + 右侧插入功能 -->
+    <div class="toolbar">
+      <div class="toolbar_left">
+        <!-- 撤销 / 重做 -->
+        <a-button class="btn" type="text" @click="undo" :disabled="historyIndex <= 0" title="撤销">
+          <img class="btn-icon" :src="UndoIcon" alt="撤销" />
+        </a-button>
+        <a-button
+          class="btn"
+          type="text"
+          @click="redo"
+          :disabled="historyIndex >= historyStack.length - 1"
+          title="重做"
+        >
+          <img class="btn-icon" :src="RedoIcon" alt="重做" />
+        </a-button>
+
+        <!-- 加粗、斜体、下划线 -->
+        <a-button class="btn" type="text" :class="{ active: isBold }" @click="toggleFormat('bold')" title="加粗">
+          <icon-bold />
+        </a-button>
+        <a-button class="btn" type="text" :class="{ active: isItalic }" @click="toggleFormat('italic')" title="斜体">
+          <icon-italic />
+        </a-button>
+        <a-button
+          class="btn"
+          type="text"
+          :class="{ active: isUnderline }"
+          @click="toggleFormat('underline')"
+          title="下划线"
+        >
+          <icon-underline />
+        </a-button>
+
+        <!-- 标题选择下拉框 -->
+        <select v-model="headingLevel" @change="applyHeading">
+          <option value="">正文</option>
+          <option value="H1">标题 1</option>
+          <option value="H2">标题 2</option>
+          <option value="H3">标题 3</option>
+          <option value="H4">标题 4</option>
+          <option value="H5">标题 5</option>
+          <option value="H6">标题 6</option>
+        </select>
+
+        <!-- 字号选择下拉框 -->
+        <select v-model="fontSize" @change="applyFontSize">
+          <option value="">默认</option>
+          <option value="12">小 (12px)</option>
+          <option value="14">常规 (14px)</option>
+          <option value="16">中 (16px)</option>
+          <option value="18">大 (18px)</option>
+          <option value="20">更大 (20px)</option>
+          <option value="24">超大 (24px)</option>
+          <option value="28">特大 (28px)</option>
+        </select>
+      </div>
+
+      <!-- 右侧:插入图表等扩展功能 -->
+      <div class="toolbar_right">
+        <div class="chat-select" ref="chartButtonRef">
+          <a-button class="insert_btn" v-if="false" @click="toggleChartPanelDropdown">
+            <img class="btn-icon" :src="ChatICON_a" alt="" />
+            插入图表&nbsp; <icon-down size="16" v-if="!showChartPanelDropdown" />
+            <icon-up size="16" v-else />
+          </a-button>
+        </div>
+        <!-- <div>
+          <button @click="insertData">插入数据</button>
+        </div> -->
+      </div>
+    </div>
+    <div class="common-config">
+      <CommonDimension :common_config="common_config" @content-changed="handleContentChanged"></CommonDimension>
+    </div>
+
+    <!-- 可编辑区域 -->
+    <div
+      ref="editorRef"
+      class="editor"
+      :contenteditable="true"
+      @dragover="onDragOverMetric"
+      @drop="onDropMetric"
+      @input="
+        (e) => {
+          onInput(e);
+          handleInputForMention(e);
+        }
+      "
+      @blur="flushPendingSave"
+      @mouseup="updateToolbar"
+      @keyup="updateToolbar"
+      @keydown="handleKeydownForMention"
+      @click="handleEditorClick"
+    ></div>
+
+    <!-- @mention 下拉菜单 -->
+    <MentionDropdown
+      :visible="showDropdown"
+      :position="dropdownPosition"
+      :options="metrics_list"
+      @select="insertOption"
+      ref="mentionDropdownRef"
+    />
+
+    <!-- mention 操作菜单 -->
+    <MentionActionMenu
+      ref="actionMenuRef"
+      :visible="showActionMenu"
+      :position="actionMenuPosition"
+      :actions="target_action"
+      :actionConfig="current_action_config"
+      :direction="actionMenuDirection"
+      @action="handleAction"
+    />
+    <ChartPanelDropdown
+      ref="chartPanelDropdownRef"
+      :visible="showChartPanelDropdown"
+      :position="chartPanelDropdownPosition"
+    />
+    <!-- <ChartPanelDropdown
+      ref="chartPanelDropdownRef"
+      :visible="showChartPanelDropdown"
+      :position="chartPanelDropdownPosition"
+      @chart-click="handleChartPanelDropdownClick"
+    /> -->
+  </div>
+  <TipsModel v-model:visible="showTips" @ok="toSaveSetting" @cancel="toNotSaveSetting" />
+</template>
+
+<script setup>
+/**
+ * 富文本编辑器组件(基于 contenteditable + document.execCommand)
+ * 支持:撤销/重做、加粗/斜体/下划线、标题、字号、光标位置记忆 + @mention 功能
+ */
+
+import { ref, onMounted, onUnmounted, nextTick, watch, computed, createApp, provide } from 'vue';
+import TableChart from '../editor-chart/TableChart.vue';
+
+import { generateUID } from './common-tools';
+
+import RedoIcon from '../../../assets/report-template/redo.svg';
+import UndoIcon from '../../../assets/report-template/undo.svg';
+import ChatICON_a from '../../../assets/report-template/chat_icon_a.svg';
+
+import MentionDropdown from '../editor-select/MentionDropdown.vue';
+import MentionActionMenu from '../editor-select/MentionActionMenu.vue';
+import ChartPanelDropdown from '../editor-select/ChartPanelDropdown.vue';
+import CommonDimension from './CommonDimension.vue';
+
+import TipsModel from './TipsModel.vue';
+
+import { useReportEditor } from '../../../base/store/use-report-editor';
+
+const reportEditor = useReportEditor();
+console.log(reportEditor);
+
+// ============ Props & Emits ============
+const props = defineProps({
+  modelValue: {
+    type: String,
+    default: ''
+  },
+  metrics_list: Array,
+  extracted_metrics: Array,
+  common_config: Object
+});
+
+const emit = defineEmits([
+  'update:modelValue',
+  'mention-click',
+  'close-filter-model',
+  'to-save-setting',
+  'submitHtml',
+  'chart-click',
+  'content-changed'
+]);
+
+// ============ Refs ============
+const editorRef = ref(null);
+const containerRef = ref(null); // 👈 新增:用于定位浮层
+const mentionDropdownRef = ref(null);
+
+// 图表数据存储,使用chartId作为唯一标识
+const chartDataStore = ref({});
+
+// 提供图表数据存储给子组件
+provide('chartDataStore', chartDataStore);
+
+// 工具栏状态
+const headingLevel = ref(''); // 当前段落是否为 H1-H6
+const fontSize = ref(''); // 当前选中字号(如 "14")
+const isBold = ref(false);
+const isItalic = ref(false);
+const isUnderline = ref(false);
+
+// ============ 历史栈(用于撤销/重做)===========
+const historyStack = ref([]); // 存储 { html, cursor } 快照
+const historyIndex = ref(-1); // 当前历史指针
+let isRestoring = false; // 是否正在恢复历史(避免重复保存)
+let pendingSaveTimeout = null; // 防抖定时器
+const SAVE_DELAY = 500; // 输入后延迟 500ms 保存
+
+// ========== Mention 功能(增量添加)==========
+const dropdownRef = ref(null);
+const actionMenuRef = ref(null);
+// ========= 图表面板功能 ==========
+const chartPanelDropdownRef = ref(null);
+const chartButtonRef = ref(null);
+
+const showDropdown = ref(false);
+const dropdownPosition = ref({ top: 0, left: 0 });
+const chartPanelDropdownPosition = ref({ top: 0, left: 0 });
+// 保存当前光标位置,用于处理点击插入图表按钮时光标丢失的问题
+const savedCursorPosition = ref(null);
+// 原始数据
+// const options = [
+//   {
+//     id: 'oU12ESIeGAD5qCw',
+//     metric_name: '管控指标',
+//     data_source_type: 1,
+//     api_description: '决策平台-管控指标',
+//     table_metadata: null,
+//     is_supports_top_bottos: 0,
+//     default_top_n: 0,
+//     default_bottom_n: 0,
+//     default_sort_order: 'desc',
+//     created_at: '1995-07-18T16:22:15',
+//     updated_at: '1995-07-18T16:22:15',
+//     flexible_template_metric_fields: [
+//       {
+//         id: 'r67yzUE8c7lfhjE',
+//         metric_id: 'oU12ESIeGAD5qCw',
+//         field_name: '组织机构',
+//         field_description: '组织机构',
+//         field_type: 'str',
+//         is_aggregatable: 0,
+//         is_enum_filterable: 1,
+//         enum_type_id: '2',
+//         is_multi_select: 0,
+//         is_and_or_condition: 0,
+//         created_at: '2019-01-23T20:40:12',
+//         updated_at: '1980-11-02T04:09:49'
+//       },
+//       {
+//         id: '6Pva5pKIyaJn25P',
+//         metric_id: 'oU12ESIeGAD5qCw',
+//         field_name: '开始时间',
+//         field_description: '开始时间',
+//         field_type: 'date',
+//         is_aggregatable: 0,
+//         is_enum_filterable: 0,
+//         enum_type_id: '0',
+//         is_multi_select: 0,
+//         is_and_or_condition: 0,
+//         created_at: '1985-11-06T11:01:42',
+//         updated_at: '2025-05-17T10:43:39'
+//       },
+//       {
+//         id: '10qOzrfpUnIgxt9',
+//         metric_id: 'oU12ESIeGAD5qCw',
+//         field_name: '结束时间',
+//         field_description: '结束时间',
+//         field_type: 'date',
+//         is_aggregatable: 0,
+//         is_enum_filterable: 0,
+//         enum_type_id: '0',
+//         is_multi_select: 0,
+//         is_and_or_condition: 0,
+//         created_at: '1987-03-29T09:04:29',
+//         updated_at: '1980-08-31T16:07:42'
+//       },
+//       {
+//         id: 'i2P3lbpYvfPZeYT',
+//         metric_id: 'oU12ESIeGAD5qCw',
+//         field_name: '查询指标',
+//         field_description: '需要查询的指标-提供枚举',
+//         field_type: 'str',
+//         is_aggregatable: 0,
+//         is_enum_filterable: 1,
+//         enum_type_id: '3',
+//         is_multi_select: 1,
+//         is_and_or_condition: 0,
+//         created_at: '2017-06-09T20:49:27',
+//         updated_at: '2024-11-14T07:21:16'
+//       }
+//     ]
+//   }
+// ];
+// 替换为你的用户列表
+
+const mentionsData = ref(props.extracted_metrics || []); // 格式为 { uid: 'mention_xxx', option }
+const mentions_metrics_Data = computed(() => {
+  return reportEditor.metrics_list;
+}); // 格式为 { uid: 'mention_xxx', option }
+
+const showActionMenu = ref(false);
+const actionMenuPosition = ref({ top: 0, left: 0 });
+const actionMenuDirection = ref('down'); // 'down' 或 'up',控制菜单方向
+const currentMentionElement = ref(null);
+const target_action = ref(null); //当前点击的指标的advanced_computing_config配置项
+// 图表面板展示
+const showChartPanelDropdown = ref(false);
+
+const showTips = ref(false);
+
+//当前指标的advanced_computing_config配置
+const current_action_config = computed(() => {
+  return reportEditor.QueryTargetMetricConfig;
+});
+
+// ============ 光标位置管理 ============
+function getCursorPath() {
+  const selection = window.getSelection();
+  if (!selection.rangeCount || !editorRef.value) return null;
+
+  const range = selection.getRangeAt(0);
+  const startContainer = range.startContainer;
+  if (!editorRef.value.contains(startContainer)) return null;
+
+  const path = [];
+  let node = startContainer;
+  while (node !== editorRef.value) {
+    const parent = node.parentNode;
+    if (!parent) break;
+    let index = 0;
+    let sibling = node.previousSibling;
+    while (sibling) {
+      index++;
+      sibling = sibling.previousSibling;
+    }
+    path.unshift(index);
+    node = parent;
+  }
+  return { path, offset: range.startOffset };
+}
+
+function setCursorPath(saved) {
+  if (!saved || !editorRef.value) return;
+  const { path, offset } = saved;
+  let node = editorRef.value;
+
+  for (let i = 0; i < path.length; i++) {
+    const idx = path[i];
+    if (idx < node.childNodes.length) {
+      node = node.childNodes[idx];
+    } else {
+      break;
+    }
+  }
+
+  if (node.nodeType === Node.ELEMENT_NODE) {
+    const textNode = findFirstTextNode(node);
+    if (textNode) {
+      setSelection(textNode, Math.min(offset, textNode.textContent.length));
+    } else {
+      setSelection(node, node.childNodes.length);
+    }
+  } else if (node.nodeType === Node.TEXT_NODE) {
+    setSelection(node, Math.min(offset, node.textContent.length));
+  }
+}
+
+function findFirstTextNode(el) {
+  for (const child of el.childNodes) {
+    if (child.nodeType === Node.TEXT_NODE && child.textContent.trim() !== '') {
+      return child;
+    }
+    if (child.nodeType === Node.ELEMENT_NODE) {
+      const found = findFirstTextNode(child);
+      if (found) return found;
+    }
+  }
+  return null;
+}
+
+function setSelection(node, offset) {
+  const range = document.createRange();
+  const sel = window.getSelection();
+  try {
+    range.setStart(node, offset);
+    range.collapse(true);
+    sel.removeAllRanges();
+    sel.addRange(range);
+  } catch (e) {
+    // 容错:节点可能已被移除
+  }
+}
+
+// ============ 历史保存逻辑 ============
+function debouncedSave() {
+  if (isRestoring) return;
+  if (pendingSaveTimeout) clearTimeout(pendingSaveTimeout);
+  pendingSaveTimeout = setTimeout(() => {
+    const content = editorRef.value.innerHTML;
+    const cursor = getCursorPath();
+    const mentionsDataSnapshot = JSON.parse(JSON.stringify(mentionsData.value));
+    saveToHistory(content, cursor, mentionsDataSnapshot);
+    pendingSaveTimeout = null;
+  }, SAVE_DELAY);
+}
+
+function immediateSave() {
+  if (isRestoring) return;
+  if (pendingSaveTimeout) {
+    clearTimeout(pendingSaveTimeout);
+    pendingSaveTimeout = null;
+  }
+  const content = editorRef.value.innerHTML;
+  const cursor = getCursorPath();
+  const mentionsDataSnapshot = JSON.parse(JSON.stringify(mentionsData.value));
+  saveToHistory(content, cursor, mentionsDataSnapshot);
+  syncMentionsData();
+}
+
+function saveToHistory(content, cursor, mentionsDataSnapshot) {
+  const current = historyStack.value[historyIndex.value];
+  if (current && current.html === content) return;
+
+  if (historyIndex.value < historyStack.value.length - 1) {
+    historyStack.value = historyStack.value.slice(0, historyIndex.value + 1);
+  }
+  historyStack.value.push({
+    html: content,
+    cursor,
+    mentionsData: mentionsDataSnapshot ? JSON.parse(JSON.stringify(mentionsDataSnapshot)) : null
+  });
+  historyIndex.value = historyStack.value.length - 1;
+}
+
+// ============ 撤销 / 重做 ============
+function undo() {
+  if (historyIndex.value <= 0 || !editorRef.value) return;
+  isRestoring = true;
+  historyIndex.value--;
+  const { html, cursor, mentionsData: savedMentionsData } = historyStack.value[historyIndex.value];
+  editorRef.value.innerHTML = html;
+  emit('submitHtml', html);
+  emit('update:modelValue', html);
+  nextTick(() => {
+    updateToolbar();
+    setCursorPath(cursor);
+    if (savedMentionsData) {
+      mentionsData.value = JSON.parse(JSON.stringify(savedMentionsData));
+    } else {
+      syncMentionsData();
+    }
+    isRestoring = false;
+  });
+}
+
+function redo() {
+  if (historyIndex.value >= historyStack.value.length - 1 || !editorRef.value) return;
+  isRestoring = true;
+  historyIndex.value++;
+  const { html, cursor, mentionsData: savedMentionsData } = historyStack.value[historyIndex.value];
+  editorRef.value.innerHTML = html;
+  emit('submitHtml', html);
+  emit('update:modelValue', html);
+  nextTick(() => {
+    updateToolbar();
+    setCursorPath(cursor);
+    if (savedMentionsData) {
+      mentionsData.value = JSON.parse(JSON.stringify(savedMentionsData));
+    } else {
+      syncMentionsData();
+    }
+    isRestoring = false;
+  });
+}
+
+function flushPendingSave() {
+  if (pendingSaveTimeout) {
+    clearTimeout(pendingSaveTimeout);
+    pendingSaveTimeout = null;
+    const content = editorRef.value.innerHTML;
+    const cursor = getCursorPath();
+    saveToHistory(content, cursor);
+  }
+}
+
+// ============ 生命周期 ============
+onMounted(() => {
+  if (editorRef.value) {
+    const initialContent = props.modelValue || '';
+    editorRef.value.innerHTML = initialContent;
+    saveToHistory(initialContent, null);
+    updateToolbar();
+    syncMentionsData(); // 👈
+  }
+
+  document.addEventListener('selectionchange', handleSelectionChange);
+  document.addEventListener('click', handleDocumentClick);
+  mentionsData.value = props.extracted_metrics;
+  emit('submitHtml', editorRef.value.innerHTML);
+});
+
+onUnmounted(() => {
+  if (pendingSaveTimeout) clearTimeout(pendingSaveTimeout);
+  document.removeEventListener('selectionchange', handleSelectionChange);
+  document.removeEventListener('click', handleDocumentClick);
+});
+
+// 处理document点击事件,关闭图表面板下拉框
+function handleDocumentClick(e) {
+  if (
+    showChartPanelDropdown.value &&
+    chartPanelDropdownRef.value &&
+    !chartPanelDropdownRef.value.$el?.contains(e.target) &&
+    !chartButtonRef.value?.contains(e.target)
+  ) {
+    showChartPanelDropdown.value = false;
+  }
+}
+
+function handleSelectionChange() {
+  if (editorRef.value && document.activeElement === editorRef.value) {
+    updateToolbar();
+  }
+}
+
+// ============ 工具栏状态同步 ============
+const updateToolbar = () => {
+  const selection = window.getSelection();
+  if (!selection.rangeCount || !editorRef.value) return;
+
+  const range = selection.getRangeAt(0);
+  let node = range.commonAncestorContainer;
+  if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
+
+  isBold.value = document.queryCommandState('bold');
+  isItalic.value = document.queryCommandState('italic');
+  isUnderline.value = document.queryCommandState('underline');
+
+  let headingNode = node;
+  while (headingNode && headingNode !== editorRef.value) {
+    if (headingNode.nodeType === Node.ELEMENT_NODE) {
+      const tagName = headingNode.tagName;
+      if (tagName === 'P' || (tagName.startsWith('H') && /^[1-6]$/.test(tagName.slice(1)))) {
+        headingLevel.value = tagName === 'P' ? '' : tagName;
+        break;
+      }
+    }
+    headingNode = headingNode.parentElement;
+  }
+  if (!headingNode || headingNode === editorRef.value) {
+    headingLevel.value = '';
+  }
+
+  let fontSizeNode = node;
+  let detectedSize = '';
+  while (fontSizeNode && fontSizeNode !== editorRef.value) {
+    if (fontSizeNode.nodeType === Node.ELEMENT_NODE) {
+      const style = window.getComputedStyle(fontSizeNode);
+      const fs = style.fontSize;
+      if (fs && fs !== '16px') {
+        detectedSize = parseInt(fs, 10).toString();
+        break;
+      }
+    }
+    fontSizeNode = fontSizeNode.parentElement;
+  }
+  fontSize.value = detectedSize;
+};
+
+// ============ 格式操作 ============
+const toggleFormat = (command) => {
+  if (!editorRef.value) return;
+  editorRef.value.focus();
+  flushPendingSave();
+
+  document.execCommand(command);
+
+  nextTick(() => {
+    const content = editorRef.value.innerHTML;
+    emit('submitHtml', content);
+    emit('update:modelValue', content);
+    emit('content-changed', content);
+    immediateSave();
+    updateToolbar();
+  });
+};
+
+const applyHeading = () => {
+  if (!editorRef.value) return;
+  editorRef.value.focus();
+  flushPendingSave();
+
+  const tagName = headingLevel.value || 'P';
+  document.execCommand('formatBlock', false, `<${tagName}>`);
+
+  nextTick(() => {
+    const content = editorRef.value.innerHTML;
+    emit('submitHtml', content);
+    emit('update:modelValue', content);
+    emit('content-changed', content);
+    immediateSave();
+    updateToolbar();
+  });
+};
+
+const applyFontSize = () => {
+  if (!editorRef.value) return;
+  editorRef.value.focus();
+  flushPendingSave();
+
+  const size = fontSize.value;
+  const selection = window.getSelection();
+  if (selection.rangeCount === 0) return;
+
+  const range = selection.getRangeAt(0);
+  const extracted = range.extractContents();
+
+  function unwrapFontSizeSpans(parent) {
+    const walker = document.createTreeWalker(parent, NodeFilter.SHOW_ELEMENT, {
+      acceptNode(node) {
+        if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'SPAN' && node.style.fontSize) {
+          return NodeFilter.FILTER_ACCEPT;
+        }
+        return NodeFilter.FILTER_SKIP;
+      }
+    });
+
+    const nodesToRemove = [];
+    let node;
+    while ((node = walker.nextNode())) {
+      nodesToRemove.push(node);
+    }
+
+    for (let i = nodesToRemove.length - 1; i >= 0; i--) {
+      const el = nodesToRemove[i];
+      const parent = el.parentNode;
+      while (el.firstChild) {
+        parent.insertBefore(el.firstChild, el);
+      }
+      parent.removeChild(el);
+    }
+  }
+
+  const fragment = document.createDocumentFragment();
+  fragment.appendChild(extracted);
+  unwrapFontSizeSpans(fragment);
+
+  if (size) {
+    const newSpan = document.createElement('span');
+    newSpan.style.fontSize = size + 'px';
+    newSpan.appendChild(fragment);
+    range.insertNode(newSpan);
+
+    const newRange = document.createRange();
+    newRange.selectNodeContents(newSpan);
+    selection.removeAllRanges();
+    selection.addRange(newRange);
+  } else {
+    range.insertNode(fragment);
+
+    const commonAncestor = range.commonAncestorContainer;
+    let container = commonAncestor.nodeType === Node.TEXT_NODE ? commonAncestor.parentElement : commonAncestor;
+
+    while (container && container !== editorRef.value) {
+      if (container.tagName && /^H[1-6]$/.test(container.tagName)) {
+        container.style.fontSize = '';
+        break;
+      }
+      container = container.parentElement;
+    }
+  }
+
+  nextTick(() => {
+    const content = editorRef.value.innerHTML;
+    emit('submitHtml', content);
+    emit('update:modelValue', content);
+    emit('content-changed', content);
+    immediateSave();
+    updateToolbar();
+  });
+};
+
+// ============ 输入监听(原有)===========
+const onInput = () => {
+  if (editorRef.value && !isRestoring) {
+    emit('submitHtml', editorRef.value.innerHTML);
+    emit('update:modelValue', editorRef.value.innerHTML);
+    emit('content-changed', editorRef.value.innerHTML);
+    debouncedSave();
+    syncMentionsData();
+  }
+};
+
+const handleContentChanged = () => {
+  emit('content-changed');
+};
+
+// ============ 外部数据更新监听 ============
+watch(
+  () => props.modelValue,
+  (newVal) => {
+    if (!editorRef.value) return;
+    const finalContent = newVal || '<p>请输入内容...</p>';
+    if (editorRef.value.innerHTML !== finalContent) {
+      editorRef.value.innerHTML = finalContent;
+      saveToHistory(finalContent, null);
+      updateToolbar();
+      syncMentionsData();
+      emit('submitHtml', finalContent);
+    }
+  }
+);
+watch(
+  () => props.extracted_metrics,
+  () => {
+    mentionsData.value = props.extracted_metrics;
+  }
+);
+watch(
+  () => mentionsData.value,
+  (list) => {
+    console.log('mentionsData', list);
+    reportEditor.updateMetricsList(list);
+  },
+  {
+    deep: true
+  }
+);
+
+// ============ 扩展功能占位 ============
+const handleSelect = (value) => {
+  console.log('Selected:', value);
+};
+
+function triggerDropdown() {
+  const range = window.getSelection()?.getRangeAt(0);
+  if (!range || !editorRef.value?.contains(range.startContainer)) return;
+
+  const rect = range.getBoundingClientRect();
+  const containerRect = containerRef.value.getBoundingClientRect();
+  const editorHeight = containerRef.value.offsetHeight;
+
+  // 预估下拉框高度(MentionDropdown 最大高度为 200px)
+  // 修复:先计算itemCount,再计算高度,确保运算符优先级正确
+  const itemCount = props.metrics_list?.length || 0;
+  const dropdownHeight = Math.min(itemCount * 30 + 20, 200);
+
+  let top = rect.bottom - containerRect.top;
+  let left = rect.left - containerRect.left;
+
+  // 检查是否超出编辑器下边界
+  if (top + dropdownHeight > editorHeight) {
+    // 显示在元素上方
+    top = rect.top - containerRect.top - dropdownHeight;
+  }
+
+  dropdownPosition.value = {
+    top: top,
+    left: left
+  };
+
+  showDropdown.value = true;
+}
+
+function insertOption(opt) {
+  const sel = window.getSelection();
+  if (sel.rangeCount === 0 || !editorRef.value) return;
+
+  const range = sel.getRangeAt(0);
+
+  if (range.startContainer.nodeType === Node.TEXT_NODE && range.startOffset > 0) {
+    const testRange = range.cloneRange();
+    testRange.setStart(range.startContainer, range.startOffset - 1);
+    const charBefore = testRange.toString();
+    if (charBefore === '@') {
+      range.setStart(range.startContainer, range.startOffset - 1);
+      range.deleteContents();
+    }
+  }
+
+  const uid = generateUID();
+
+  const placeholder = document.createElement('span');
+  // placeholder.textContent = `{{${opt.metric_name}}} `;
+  placeholder.setAttribute('contenteditable', 'false');
+  placeholder.setAttribute('data-mention', 'true');
+  placeholder.setAttribute('data-uid', uid);
+  placeholder.style.cssText = `
+    display: inline-flex;
+    align-items: center;
+    background-color: var(--question_bg);
+    border-radius: 4px;
+    padding: 0 4px;
+    margin: 0 2px;}
+    user-select: none;
+    -webkit-user-select: none;
+    cursor: pointer;
+    color: var(--primary-default);
+  `;
+  const svg_icon = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+  fill="none" version="1.1" width="16" height="16" viewBox="0 0 16 16"><defs><clipPath id="master_svg0_2_674"><rect x="0" y="0"
+  width="16" height="26" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_2_674)"><g>
+  <path d="M12.30009745625,5.94999981C12.15009785625,5.799999714,11.95009705625,5.75,11.75009725625,5.75L4.25009751625
+  ,5.800000191C4.05009770625,5.800000191,3.85009741625,5.85000038,3.70009755625,6C3.40009760825,6.30000019,3.40009760825,6.75,3.70009755625,
+  7.0500001999999995L7.45009735625,10.8000002C7.50009725625,10.850000399999999,7.60009765625,10.9000006,7.65009685625,10.9499998C7.65009685625,
+  10.9499998,7.70009705625,11,7.70009705625,11C7.95009705625,11.100000399999999,8.30009745625,11.0500002,8.500097256250001,10.850000399999999L12.25009725625,
+  7.0500001999999995C12.60009765625,6.69999981,12.55009745625,6.25,12.30009745625,5.94999981Z" fill="var(--primary-default)" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>`;
+  placeholder.innerHTML = `{{${opt.field_name}}}&nbsp;&nbsp;${svg_icon}`;
+  const zeroWidth = '\u200B';
+  const after = document.createTextNode(zeroWidth);
+  range.insertNode(after);
+  range.insertNode(placeholder);
+
+  const newRange = document.createRange();
+  newRange.setStartAfter(after);
+  newRange.collapse(true);
+  sel.removeAllRanges();
+  sel.addRange(newRange);
+  // 保存到MentionData列表中
+
+  mentionsData.value.push({
+    uid,
+    option: opt
+  });
+
+  hideDropdown();
+  hideActionMenu();
+  immediateSave(); // 👈 接入你的历史机制
+  emit('submitHtml', editorRef.value.innerHTML);
+  emit('content-changed', editorRef.value.innerHTML);
+}
+// 当编辑器内容改变时,同步数据
+function syncMentionsData() {
+  if (!editorRef.value) return;
+
+  // 获取当前 DOM 中所有 mention 元素的 UID 集合
+  const currentUIDs = new Set();
+  const mentionsInDOM = editorRef.value.querySelectorAll('[data-mention][data-uid]');
+  // 使用for循环替代forEach以提高性能
+  for (let i = 0, len = mentionsInDOM.length; i < len; i++) {
+    const uid = mentionsInDOM[i].getAttribute('data-uid');
+    if (uid) currentUIDs.add(uid);
+  }
+  // 过滤 mentionsData,只保留还在 DOM 中的
+  mentionsData.value = mentionsData.value.filter((item) => currentUIDs.has(item.uid));
+}
+function handleInputForMention(e) {
+  if (isRestoring) return;
+
+  const selection = window.getSelection();
+  if (!selection?.rangeCount || !editorRef.value) return;
+
+  const range = selection.getRangeAt(0);
+  if (!editorRef.value.contains(range.startContainer)) return;
+
+  let charBeforeCursor = '';
+  if (range.startContainer.nodeType === Node.TEXT_NODE && range.startOffset > 0) {
+    const testRange = range.cloneRange();
+    testRange.setStart(range.startContainer, range.startOffset - 1);
+    charBeforeCursor = testRange.toString();
+  }
+
+  if (charBeforeCursor === '@') {
+    if (!showDropdown.value) {
+      triggerDropdown();
+    }
+  } else {
+    hideDropdown();
+    hideActionMenu();
+  }
+}
+
+function handleKeydownForMention(e) {
+  if (showDropdown.value && mentionDropdownRef.value) {
+    mentionDropdownRef.value.handleKeydown(e);
+    if (e.key === 'Escape') {
+      hideDropdown();
+      hideActionMenu();
+    }
+    // 如果已处理(如 Enter),阻止默认
+    if (['ArrowUp', 'ArrowDown', 'Enter'].includes(e.key)) {
+      e.preventDefault();
+    }
+  } else if (e.key === 'Escape') {
+    hideDropdown();
+    hideActionMenu();
+  }
+}
+
+function hideDropdown() {
+  showDropdown.value = false;
+}
+
+function hideActionMenu() {
+  showActionMenu.value = false;
+  currentMentionElement.value = null;
+}
+// 编辑器中的点击事件合集
+function handleEditorClick(e) {
+  console.log('handleEditorClick', e.target);
+  console.log('handleEditorClick', actionMenuRef.value.$el);
+  // console.log('handleEditorClick', actionMenuRef.value?.contains(e.target));
+  if (actionMenuRef.value && !actionMenuRef.value.$el?.contains(e.target)) {
+    hideActionMenu();
+  }
+
+  // 关闭图表面板下拉框
+  if (
+    chartPanelDropdownRef.value &&
+    !chartPanelDropdownRef.value.$el?.contains(e.target) &&
+    !chartButtonRef.value?.contains(e.target)
+  ) {
+    showChartPanelDropdown.value = false;
+  }
+  // console.log('reportEditor.is_need_save', reportEditor.QueryNeedSave);
+  // if (reportEditor.QueryNeedSave) {
+  //   showTips.value = true;
+  //   return;
+  // }
+
+  // 移除所有已有的高亮样式
+  const allMentions = editorRef.value.querySelectorAll('[data-mention]');
+  const allCharts = editorRef.value.querySelectorAll('[data-chart]');
+  allMentions.forEach((mention) => {
+    mention.classList.remove('mention-highlighted');
+  });
+  allCharts.forEach((chart) => {
+    chart.classList.remove('chart-selected');
+  });
+  // 关闭过滤器
+  closeFilterModel();
+
+  // 如果点击的是提及
+  const mention = e.target.closest('[data-mention]');
+  if (mention) {
+    const uid = mention?.getAttribute('data-uid');
+    const option = mentionsData.value.find((item) => item.uid === uid);
+    reportEditor.setTargetMetricId(uid);
+
+    // 添加高亮样式
+    mention.classList.add('mention-highlighted');
+    // console.log(option.option.advanced_computing_config);
+    // console.log(JSON.parse(option.option.advanced_computing_config));
+    console.log(option.option, 999);
+    target_action.value = JSON.parse(option.option.source.advanced_computing_config);
+    // 保存当前指标引用,用于后续位置调整
+    currentMentionElement.value = mention;
+
+    // 给父组件一个触发一个事件,显示过滤器
+    emit('mention-click', option);
+
+    // 使用 setTimeout 确保过滤器组件已经显示,富文本宽度已经调整
+    setTimeout(() => {
+      if (!currentMentionElement.value) return;
+
+      // 重新获取指标元素
+      const mention = currentMentionElement.value;
+
+      // 1. 先滚动到指标位置
+      mention.scrollIntoView({ behavior: 'smooth', block: 'center' });
+
+      // 2. 滚动后再重新计算位置和高度
+      setTimeout(() => {
+        // 重新获取元素位置,因为滚动后位置可能变化
+        const rect = mention.getBoundingClientRect();
+        const containerRect = containerRef.value.getBoundingClientRect();
+        const editorHeight = containerRef.value.offsetHeight;
+
+        // 预估菜单高度:一级菜单约 120px,二级菜单约 200px
+        const primaryMenuHeight = 120; // 一级菜单高度
+        const secondaryMenuHeight = 200; // 二级菜单最大高度
+
+        let top = rect.bottom - containerRect.top;
+        let direction = 'down';
+
+        // 使用二级菜单高度判断是否超出边界
+        if (top + secondaryMenuHeight > editorHeight) {
+          // 显示在元素上方,使用一级菜单高度计算位置
+          top = rect.top - containerRect.top - primaryMenuHeight - 10;
+          direction = 'up';
+        }
+
+        // 设置菜单方向
+        actionMenuDirection.value = direction;
+
+        actionMenuPosition.value = {
+          top: top,
+          left: rect.left - containerRect.left
+        };
+
+        nextTick(() => {
+          showActionMenu.value = true;
+        });
+      }, 300); // 增加延迟,确保滚动完成
+    }, 300); // 增加延迟,确保过滤器组件完全显示
+
+    e.stopPropagation();
+  }
+  // 如果点击的是图表
+  const chart = e.target.closest('[data-chart]');
+  if (chart) {
+    const chartId = chart.getAttribute('data-chart-id');
+    const chartType = chart.getAttribute('data-chart-type');
+
+    // 移除所有图表的选中状态
+    const allCharts = editorRef.value.querySelectorAll('[data-chart]');
+    allCharts.forEach((c) => {
+      c.classList.remove('chart-selected');
+    });
+
+    // 添加选中状态
+    chart.classList.add('chart-selected');
+
+    // 提交点击事件
+    emit('chart-click', {
+      chartId,
+      chartType,
+      chartData: chartDataStore.value[chartId]
+    });
+
+    e.stopPropagation();
+  }
+}
+const closeFilterModel = () => {
+  emit('close-filter-model');
+};
+
+const toSaveSetting = () => {
+  showTips.value = false;
+  emit('to-save-setting');
+};
+const toNotSaveSetting = () => {
+  showTips.value = false;
+};
+
+// 检查是否在受限节点内(data-mention 或 data-chart 节点)
+function isInsideRestrictedNode(element) {
+  let current = element;
+  while (current && current !== editorRef.value) {
+    if (current.nodeType === Node.TEXT_NODE) {
+      current = current.parentElement;
+      if (!current) break;
+    }
+    if (current.dataset.mention === 'true' || current.dataset.chart === 'true') {
+      return true;
+    }
+    current = current.parentElement;
+  }
+  return false;
+}
+
+// ============ 拖拽接收逻辑 ============
+const onDragOverMetric = (event) => {
+  // 可选:只允许特定类型拖入
+  event.preventDefault();
+  event.stopPropagation();
+
+  // 检查是否在受限节点内
+  if (isInsideRestrictedNode(event.target)) {
+    event.dataTransfer.dropEffect = 'none';
+    return;
+  }
+
+  if (editorRef.value && document.activeElement !== editorRef.value) {
+    editorRef.value.focus({ preventScroll: true });
+  }
+  event.dataTransfer.dropEffect = 'copy'; // 不再判断类型
+};
+
+// 根据 drop 坐标获取富文本内的有效 Range
+function getDropRangeInEditor(x, y) {
+  if (!editorRef.value) return null;
+
+  // 1. 使用标准 API(Chrome / Safari)
+  if (document.caretRangeFromPoint) {
+    const range = document.caretRangeFromPoint(x, y);
+    if (range && editorRef.value.contains(range.startContainer)) {
+      return range;
+    }
+  }
+
+  // 2. Firefox 或 fallback:用 elementFromPoint
+  const el = document.elementFromPoint(x, y);
+  if (!el || !editorRef.value.contains(el)) return null;
+
+  // 如果点中的是 mention 元素(不可编辑),找最近的可插入位置
+  let target = el.closest('[contenteditable="false"]') ? el.parentElement : el;
+
+  // 确保 target 在 editor 内
+  while (target && target !== editorRef.value && !target.isEqualNode(editorRef.value)) {
+    if (target.nodeType === Node.ELEMENT_NODE) break;
+    target = target.parentNode;
+  }
+
+  if (!target || target === editorRef.value) {
+    // 直接插入到编辑器末尾
+    return null;
+  }
+
+  const range = document.createRange();
+  try {
+    range.selectNodeContents(target);
+    range.collapse(false); // 插入到元素末尾
+  } catch (e) {
+    return null;
+  }
+
+  return range;
+}
+
+const onDropMetric = (event) => {
+  event.preventDefault();
+  event.stopPropagation();
+
+  // 检查是否在受限节点内
+  if (isInsideRestrictedNode(event.target)) {
+    return;
+  }
+
+  // 强制聚焦富文本(即使没光标)
+  if (editorRef.value && document.activeElement !== editorRef.value) {
+    editorRef.value.focus({ preventScroll: true });
+  }
+  const text = event.dataTransfer.getData('application/x-metric');
+  if (!text) return;
+  let item;
+  try {
+    item = JSON.parse(text);
+    console.log(item, 9999);
+    if (!item || !item.field_name) return;
+  } catch (e) {
+    console.warn('Drop parse error:', e);
+    return;
+  }
+
+  // 👇 关键:根据鼠标位置获取插入点
+  let range = getDropRangeInEditor(event.clientX, event.clientY);
+
+  if (!range) {
+    // Fallback: 插入到富文本末尾
+    const editor = editorRef.value;
+    const lastChild = editor.lastChild;
+
+    range = document.createRange();
+    if (lastChild) {
+      range.selectNodeContents(lastChild);
+      range.collapse(false);
+    } else {
+      // 编辑器完全为空
+      range.selectNodeContents(editor);
+      range.collapse(true);
+    }
+  }
+
+  // 设置 selection 到目标位置
+  const sel = window.getSelection();
+  sel.removeAllRanges();
+  sel.addRange(range);
+
+  // 调用你已有的 insertOption(它会基于当前 selection 插入)
+  insertOption(item);
+};
+function handleAction(action) {
+  const mention = currentMentionElement.value;
+  if (!mention || !editorRef.value) return;
+
+  switch (action) {
+    case 'delete':
+      mention.remove();
+      break;
+
+    case 'edit':
+      const rawText = mention.textContent.slice(1, -1);
+      const textNode = document.createTextNode(`@${rawText}`);
+      mention.replaceWith(textNode);
+
+      const range = document.createRange();
+      range.setStartAfter(textNode);
+      range.collapse(true);
+      const sel = window.getSelection();
+      sel.removeAllRanges();
+      sel.addRange(range);
+
+      nextTick(() => {
+        editorRef.value.dispatchEvent(new Event('input', { bubbles: true }));
+      });
+      return;
+
+    case 'view':
+      const name = mention.textContent.slice(1, -1);
+      alert(`查看详情: ${name}`);
+      return;
+  }
+
+  hideActionMenu();
+  immediateSave();
+  emit('submitHtml', editorRef.value.innerHTML);
+  emit('content-changed', editorRef.value.innerHTML);
+}
+
+// 处理图表面板下拉菜单点击-插入图表节点
+function handleChartPanelDropdownClick(item) {
+  showChartPanelDropdown.value = false;
+
+  if (!editorRef.value) return;
+
+  // 强制聚焦富文本
+  editorRef.value.focus({ preventScroll: true });
+
+  const selection = window.getSelection();
+  const editor = editorRef.value;
+  let insertRange;
+
+  // 优先使用保存的光标位置
+  if (savedCursorPosition.value && editor.contains(savedCursorPosition.value.startContainer)) {
+    insertRange = savedCursorPosition.value;
+  } else if (selection.rangeCount && editor.contains(selection.getRangeAt(0).startContainer)) {
+    const currentRange = selection.getRangeAt(0);
+
+    // 判断光标是否在编辑器开头
+    let isAtStart = false;
+
+    // 情况1:编辑器为空
+    if (editor.innerHTML.trim() === '') {
+      isAtStart = true;
+    } else {
+      // 情况2:光标在第一个节点的开头
+      const firstChild = editor.firstChild;
+      if (firstChild) {
+        // 获取编辑器的第一个文本节点
+        let firstTextNode = null;
+        let node = firstChild;
+
+        // 查找第一个文本节点
+        while (node && !firstTextNode) {
+          if (node.nodeType === Node.TEXT_NODE) {
+            firstTextNode = node;
+          } else if (node.nodeType === Node.ELEMENT_NODE) {
+            if (node.firstChild) {
+              node = node.firstChild;
+            } else {
+              // 空元素,直接判断
+              firstTextNode = node;
+            }
+          } else {
+            node = node.nextSibling;
+          }
+        }
+
+        // 比较当前光标位置与第一个文本节点的位置
+        if (firstTextNode) {
+          isAtStart = currentRange.startContainer === firstTextNode && currentRange.startOffset === 0;
+        }
+      }
+    }
+
+    if (isAtStart) {
+      // 光标在开头,插入到末尾
+      insertRange = document.createRange();
+      const lastChild = editor.lastChild;
+
+      if (lastChild) {
+        insertRange.selectNodeContents(lastChild);
+        insertRange.collapse(false); // 折叠到末尾
+      } else {
+        insertRange.selectNodeContents(editor);
+        insertRange.collapse(true);
+      }
+    } else {
+      // 光标不在开头,使用当前光标位置
+      insertRange = currentRange;
+    }
+  } else {
+    // 没有有效光标位置,插入到末尾
+    insertRange = document.createRange();
+    const lastChild = editor.lastChild;
+
+    if (lastChild) {
+      insertRange.selectNodeContents(lastChild);
+      insertRange.collapse(false); // 折叠到末尾
+    } else {
+      insertRange.selectNodeContents(editor);
+      insertRange.collapse(true);
+    }
+  }
+
+  // 创建图表div元素
+  const chartDiv = document.createElement('div');
+  const chartId = generateUID();
+  chartDiv.className = 'chart-container';
+  chartDiv.setAttribute('data-chart-id', chartId);
+  chartDiv.setAttribute('data-chart-type', item.type);
+  chartDiv.setAttribute('contenteditable', false);
+  chartDiv.setAttribute('data-chart', true);
+  chartDiv.style.cssText = `
+    display: inline-block;
+    border: 1px dashed #d9d9d9;
+    border-radius: 4px;
+    padding: 20px;
+    margin: 10px 0;
+    background-color: #fafafa;
+    width: calc(99% - 40px);
+    min-height: 200px;
+    text-align: center;
+    cursor: pointer;
+    vertical-align: bottom;
+  `;
+
+  // 先插入图表div
+  insertRange.insertNode(chartDiv);
+
+  // 初始化图表数据并存储到chartDataStore中
+  chartDataStore.value[chartId] = {
+    type: item.type,
+    data: [],
+    columns: [],
+    isLoading: false
+  };
+
+  // 如果是表格类型,初始加载TableChart组件
+  if (item.type === 'table') {
+    // 创建表格组件实例
+    const app = createApp(TableChart, {
+      chartId: chartId
+    });
+    // 提供chartDataStore给子组件
+    app.provide('chartDataStore', chartDataStore);
+    // 挂载到chartDiv
+    app.mount(chartDiv);
+  } else {
+    // 其他图表类型显示默认内容
+    chartDiv.innerHTML = `
+      // <div style="font-size: 14px; color: #666; margin-bottom: 10px;">${item.name}</div>
+      // <div style="font-size: 12px; color: #999;">点击编辑图表</div>
+    `;
+  }
+
+  // 创建零宽度空格节点,用于定位光标
+  // const zeroWidthSpace = document.createTextNode('\u200B');
+
+  // 然后在图表后面插入零宽度空格节点
+  // chartDiv.parentNode.insertBefore(zeroWidthSpace, chartDiv.nextSibling);
+
+  // 确保光标在零宽度空格后面
+  const newRange = document.createRange();
+  // newRange.setStartAfter(zeroWidthSpace);
+  newRange.collapse(true);
+
+  // 清除所有选择范围并添加新范围
+  selection.removeAllRanges();
+  selection.addRange(newRange);
+
+  // 滚动到插入的图表位置
+  chartDiv.scrollIntoView({ behavior: 'smooth', block: 'center' });
+
+  // 重置保存的光标位置
+  savedCursorPosition.value = null;
+
+  // 保存到历史记录
+  immediateSave();
+
+  // 更新父组件
+  emit('submitHtml', editor.innerHTML);
+  emit('update:modelValue', editor.innerHTML);
+  emit('content-changed', editor.innerHTML);
+}
+
+const insertData = () => {
+  const columns = [
+    {
+      title: 'Name',
+      dataIndex: 'name'
+    },
+    {
+      title: 'Salary',
+      dataIndex: 'salary'
+    },
+    {
+      title: 'Address',
+      dataIndex: 'address'
+    },
+    {
+      title: 'Email',
+      dataIndex: 'email'
+    }
+  ];
+  const data = [
+    {
+      key: '1',
+      name: 'Jane Doe',
+      salary: 23000,
+      address: '32 Park Road, London',
+      email: 'jane.doe@example.com'
+    },
+    {
+      key: '2',
+      name: 'Alisa Ross',
+      salary: 25000,
+      address: '35 Park Road, London',
+      email: 'alisa.ross@example.com'
+    },
+    {
+      key: '3',
+      name: 'Kevin Sandra',
+      salary: 22000,
+      address: '31 Park Road, London',
+      email: 'kevin.sandra@example.com'
+    },
+    {
+      key: '4',
+      name: 'Ed Hellen',
+      salary: 17000,
+      address: '42 Park Road, London',
+      email: 'ed.hellen@example.com'
+    },
+    {
+      key: '5',
+      name: 'William Smith',
+      salary: 27000,
+      address: '62 Park Road, London',
+      email: 'william.smith@example.com'
+    }
+  ];
+  for (const key in chartDataStore.value) {
+    setChartLoading(key, true);
+    setTimeout(() => {
+      updateChartData(key, data, columns);
+    }, 2000);
+  }
+};
+// 更新图表数据的示例函数
+function updateChartData(chartId, data, columns) {
+  if (chartDataStore.value[chartId]) {
+    chartDataStore.value[chartId].data = data;
+    chartDataStore.value[chartId].columns = columns;
+    chartDataStore.value[chartId].isLoading = false;
+  }
+}
+
+// 设置图表加载状态的示例函数
+function setChartLoading(chartId, isLoading) {
+  if (chartDataStore.value[chartId]) {
+    chartDataStore.value[chartId].isLoading = isLoading;
+  }
+}
+
+// 切换图表面板下拉框显示状态
+function toggleChartPanelDropdown() {
+  showChartPanelDropdown.value = !showChartPanelDropdown.value;
+  if (showChartPanelDropdown.value) {
+    // 保存当前光标位置,防止点击按钮后光标丢失
+    const selection = window.getSelection();
+    if (selection.rangeCount && editorRef.value.contains(selection.getRangeAt(0).startContainer)) {
+      savedCursorPosition.value = selection.getRangeAt(0).cloneRange();
+    }
+    calculateChartPanelPosition();
+  }
+}
+
+// 计算图表面板下拉框位置
+function calculateChartPanelPosition() {
+  if (!chartButtonRef.value || !containerRef.value) return;
+
+  const buttonRect = chartButtonRef.value.getBoundingClientRect();
+  const containerRect = containerRef.value.getBoundingClientRect();
+
+  chartPanelDropdownPosition.value = {
+    top: buttonRect.bottom - containerRect.top,
+    left: buttonRect.left - containerRect.left
+  };
+}
+</script>
+
+<style scoped>
+/* 原有样式保持不变 */
+.rich-text-editor {
+  border: 1px solid #d8d8d8;
+  border-radius: 20px;
+  width: calc(100% - 30px);
+  height: calc(100% - 72px);
+  font-family: sans-serif;
+  margin: 0px 10px 10px 20px;
+  box-sizing: border-box;
+  position: relative; /* 👈 关键:使浮层定位基于此 */
+}
+.common-config {
+  height: 50px;
+  padding: 5px 20px;
+  box-sizing: border-box;
+}
+
+.toolbar {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.toolbar_left,
+.toolbar_right {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  gap: 8px;
+  padding: 8px;
+}
+
+.toolbar .btn-icon {
+  width: 20px;
+}
+.toolbar .toolbar_left button {
+  border: none;
+  padding: 5px;
+}
+
+.toolbar .btn.active {
+  color: var(--primary-default);
+  background-color: rgba(0, 0, 0, 0.05);
+  border-radius: 4px;
+}
+
+.editor {
+  height: calc(100% - 68px - 50px);
+  min-height: 150px;
+  overflow-y: auto;
+  padding: 12px;
+  padding-top: 0px;
+  outline: none;
+  line-height: 1.5;
+  pointer-events: auto;
+  user-select: text;
+}
+
+.editor:focus {
+  outline: none;
+}
+
+:deep(.editor img) {
+  width: 100% !important;
+  height: auto !important;
+  display: block;
+  max-width: 100%;
+  object-fit: contain;
+}
+
+.toolbar select {
+  padding: 6px 10px;
+  font-size: 14px;
+  border: 1px solid #ccc;
+  border-radius: 6px;
+  background-color: #fff;
+  cursor: pointer;
+  outline: none;
+  appearance: none;
+  background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2'%3e%3cpath d='M6 9l6 6 6-6'/%3e%3c/svg%3e");
+  background-repeat: no-repeat;
+  background-position: right 8px center;
+  background-size: 12px;
+  padding-right: 30px;
+  min-width: 90px;
+  box-sizing: border-box;
+}
+
+.toolbar select:hover {
+  border-color: var(--bg-title);
+}
+
+.toolbar select:focus {
+  border-color: var(--primary-default);
+  box-shadow: 0 0 0 2px var(--bg-title);
+}
+
+.toolbar .chat-select {
+  width: 120px;
+}
+
+.chat-select .insert_btn {
+  width: 100%;
+}
+
+.insert_btn .btn-icon {
+  width: 16px;
+  margin-right: 5px;
+}
+
+.arco-dropdown-open .arco-icon-down {
+  transform: rotate(180deg);
+}
+
+:deep(.editor h1) {
+  font-size: 28px;
+}
+:deep(.editor h2) {
+  font-size: 20px;
+}
+:deep(.editor h3) {
+  font-size: 16px;
+}
+
+/* ========== Mention 样式 ==========
+.floating-dropdown {
+  position: absolute;
+  z-index: 1000;
+  background: white;
+  border: 1px solid #d9d9d9;
+  border-radius: 4px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+  min-width: 120px;
+  max-height: 200px;
+  overflow-y: auto;
+}
+
+/* 指标高亮样式 */
+:deep(.editor [data-mention].mention-highlighted) {
+  background-color: var(--primary-light-1);
+  border: 1px solid var(--primary-default);
+  box-shadow: 0 0 0 2px var(--primary-light-2);
+}
+
+/* 图表选中样式 */
+:deep(.editor [data-chart].chart-selected) {
+  border: 2px solid var(--primary-default) !important;
+  background-color: var(--primary-light-1) !important;
+  box-shadow: 0 0 0 3px var(--primary-light-2) !important;
+}
+
+.dropdown-option {
+  display: block;
+  width: 100%;
+  text-align: left;
+  padding: 6px 10px;
+  border: none;
+  background: white;
+  cursor: pointer;
+}
+
+.dropdown-option:hover {
+  background-color: #f5f5f5;
+}
+
+/* .mention-action-menu {
+  position: absolute;
+  z-index: 1001;
+  background: white;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
+  min-width: 100px;
+}
+
+.mention-action-menu button {
+  display: block;
+  width: 100%;
+  padding: 6px 10px;
+  border: none;
+  background: white;
+  text-align: left;
+  cursor: pointer;
+}
+
+.mention-action-menu button:hover {
+  background-color: #f0f0f0;
+} */
+</style>

+ 196 - 13
src/modules/report-template/editor-model/EditorDetail.vue

@@ -33,6 +33,13 @@
         >
           <icon-underline />
         </a-button>
+        <!-- 列表按钮 -->
+        <a-button class="btn" type="text" @click="toggleList('ul')" title="无序列表">
+          <icon-unordered-list />
+        </a-button>
+        <a-button class="btn" type="text" @click="toggleList('ol')" title="有序列表">
+          <icon-ordered-list />
+        </a-button>
 
         <!-- 标题选择下拉框 -->
         <select v-model="headingLevel" @change="applyHeading">
@@ -61,7 +68,7 @@
       <!-- 右侧:插入图表等扩展功能 -->
       <div class="toolbar_right">
         <div class="chat-select" ref="chartButtonRef">
-          <a-button class="insert_btn" @click="toggleChartPanelDropdown">
+          <a-button class="insert_btn" v-if="false" @click="toggleChartPanelDropdown">
             <img class="btn-icon" :src="ChatICON_a" alt="" />
             插入图表&nbsp; <icon-down size="16" v-if="!showChartPanelDropdown" />
             <icon-up size="16" v-else />
@@ -73,7 +80,7 @@
       </div>
     </div>
     <div class="common-config">
-      <CommonDimension @content-changed="handleContentChanged"></CommonDimension>
+      <CommonDimension :common_config="common_config" @content-changed="handleContentChanged"></CommonDimension>
     </div>
 
     <!-- 可编辑区域 -->
@@ -92,7 +99,7 @@
       @blur="flushPendingSave"
       @mouseup="updateToolbar"
       @keyup="updateToolbar"
-      @keydown="handleKeydownForMention"
+      @keydown="handleKeydown"
       @click="handleEditorClick"
     ></div>
 
@@ -164,7 +171,8 @@ const props = defineProps({
     default: ''
   },
   metrics_list: Array,
-  extracted_metrics: Array
+  extracted_metrics: Array,
+  common_config: Object
 });
 
 const emit = defineEmits([
@@ -432,6 +440,14 @@ function saveToHistory(content, cursor, mentionsDataSnapshot) {
 // ============ 撤销 / 重做 ============
 function undo() {
   if (historyIndex.value <= 0 || !editorRef.value) return;
+
+  // 检查撤销后的内容是否为空
+  const nextHistoryIndex = historyIndex.value - 1;
+  const nextHistory = historyStack.value[nextHistoryIndex];
+  if (nextHistory && !nextHistory.html.trim()) {
+    return;
+  }
+
   isRestoring = true;
   historyIndex.value--;
   const { html, cursor, mentionsData: savedMentionsData } = historyStack.value[historyIndex.value];
@@ -588,7 +604,64 @@ const applyHeading = () => {
   flushPendingSave();
 
   const tagName = headingLevel.value || 'P';
-  document.execCommand('formatBlock', false, `<${tagName}>`);
+  const selection = window.getSelection();
+
+  if (!selection.rangeCount) {
+    document.execCommand('formatBlock', false, `<${tagName}>`);
+  } else {
+    // 获取当前选区
+    const range = selection.getRangeAt(0);
+
+    // 检查选区内是否有指标元素
+    const hasMentions = range.cloneContents().querySelectorAll('[data-mention]').length > 0;
+
+    // 查找包含选区的块级元素
+    let blockElement = range.commonAncestorContainer;
+    while (blockElement && blockElement !== editorRef.value) {
+      if (
+        blockElement.nodeType === Node.ELEMENT_NODE &&
+        (blockElement.tagName === 'P' ||
+          blockElement.tagName === 'DIV' ||
+          (blockElement.tagName.startsWith('H') && /^[1-6]$/.test(blockElement.tagName.slice(1))))
+      ) {
+        break;
+      }
+      blockElement = blockElement.parentElement;
+    }
+
+    if (hasMentions) {
+      // 如果选区内有指标元素,使用更可靠的方法
+      // 1. 创建一个新的块级元素
+      const newBlock = document.createElement(tagName);
+
+      // 2. 将选区内的内容复制到新的块级元素中
+      const content = range.extractContents();
+      newBlock.appendChild(content);
+
+      // 3. 用新的块级元素替换原始选区
+      if (blockElement && blockElement !== editorRef.value) {
+        // 如果找到了包含选区的块级元素,替换它
+        blockElement.replaceWith(newBlock);
+      } else {
+        // 否则,直接插入新的块级元素
+        range.insertNode(newBlock);
+      }
+
+      // 4. 恢复选区
+      const newRange = document.createRange();
+      try {
+        newRange.setStart(newBlock.firstChild || newBlock, 0);
+        newRange.collapse(true);
+        selection.removeAllRanges();
+        selection.addRange(newRange);
+      } catch (e) {
+        // 容错:如果节点结构发生变化,无法恢复选区
+      }
+    } else {
+      // 如果选区内没有指标元素,使用标准方法
+      document.execCommand('formatBlock', false, `<${tagName}>`);
+    }
+  }
 
   nextTick(() => {
     const content = editorRef.value.innerHTML;
@@ -803,7 +876,8 @@ function insertOption(opt) {
   10.9499998,7.70009705625,11,7.70009705625,11C7.95009705625,11.100000399999999,8.30009745625,11.0500002,8.500097256250001,10.850000399999999L12.25009725625,
   7.0500001999999995C12.60009765625,6.69999981,12.55009745625,6.25,12.30009745625,5.94999981Z" fill="var(--primary-default)" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>`;
   placeholder.innerHTML = `{{${opt.field_name}}}&nbsp;&nbsp;${svg_icon}`;
-  const zeroWidth = '\u200B';
+  const zeroWidth = ' ';
+  // const zeroWidth = '\u200B';
   const after = document.createTextNode(zeroWidth);
   range.insertNode(after);
   range.insertNode(placeholder);
@@ -884,6 +958,29 @@ function handleKeydownForMention(e) {
   }
 }
 
+function handleKeydown(e) {
+  // 先处理mention相关的键盘事件
+  handleKeydownForMention(e);
+
+  // 处理Tab键缩进
+  if (e.key === 'Tab') {
+    e.preventDefault();
+
+    // 获取当前选区
+    const selection = window.getSelection();
+    if (!selection.rangeCount) return;
+
+    const range = selection.getRangeAt(0);
+
+    // 插入4个空格作为缩进
+    const indent = '    ';
+    document.execCommand('insertText', false, indent);
+
+    // 保存到历史记录
+    immediateSave();
+  }
+}
+
 function hideDropdown() {
   showDropdown.value = false;
 }
@@ -1123,7 +1220,7 @@ const onDropMetric = (event) => {
   let item;
   try {
     item = JSON.parse(text);
-    console.log(item, 9999);
+    // console.log(item, 9999);
     if (!item || !item.field_name) return;
   } catch (e) {
     console.warn('Drop parse error:', e);
@@ -1441,6 +1538,92 @@ function setChartLoading(chartId, isLoading) {
   }
 }
 
+// 实现有序列表和无序列表的切换功能
+function toggleList(listType) {
+  if (!editorRef.value) return;
+
+  editorRef.value.focus();
+  flushPendingSave();
+
+  const selection = window.getSelection();
+  if (!selection.rangeCount) return;
+
+  const range = selection.getRangeAt(0);
+  let currentNode = range.startContainer;
+
+  // 找到包含选择起始点的列表项或列表
+  let listItem = null;
+  let list = null;
+
+  while (currentNode && currentNode !== editorRef.value) {
+    if (currentNode.nodeName === 'LI') {
+      listItem = currentNode;
+      list = currentNode.parentElement;
+      break;
+    }
+    if (currentNode.nodeName === 'UL' || currentNode.nodeName === 'OL') {
+      list = currentNode;
+      break;
+    }
+    currentNode = currentNode.nodeType === Node.TEXT_NODE ? currentNode.parentElement : currentNode.parentElement;
+  }
+
+  // 检查当前是否已经在目标类型的列表中
+  if (list && ((listType === 'ul' && list.nodeName === 'UL') || (listType === 'ol' && list.nodeName === 'OL'))) {
+    // 已经在目标类型的列表中,切换回正文
+    const paragraphs = [];
+
+    // 为每个列表项创建一个段落
+    for (let i = 0; i < list.children.length; i++) {
+      const listItem = list.children[i];
+      const paragraph = document.createElement('P');
+
+      // 复制所有子节点到段落
+      while (listItem.firstChild) {
+        paragraph.appendChild(listItem.firstChild);
+      }
+
+      paragraphs.push(paragraph);
+    }
+
+    // 替换列表为段落
+    const parent = list.parentNode;
+    const nextSibling = list.nextSibling;
+
+    // 移除列表
+    parent.removeChild(list);
+
+    // 插入段落
+    paragraphs.forEach((paragraph) => {
+      parent.insertBefore(paragraph, nextSibling);
+    });
+
+    // 恢复选择
+    if (paragraphs.length > 0) {
+      range.setStart(paragraphs[0], 0);
+      range.collapse(true);
+      selection.removeAllRanges();
+      selection.addRange(range);
+    }
+  } else {
+    // 不在目标类型的列表中,创建相应的列表
+    if (listType === 'ul') {
+      document.execCommand('insertUnorderedList', false, null);
+    } else if (listType === 'ol') {
+      document.execCommand('insertOrderedList', false, null);
+    }
+  }
+
+  nextTick(() => {
+    const content = editorRef.value.innerHTML;
+    emit('submitHtml', content);
+    emit('update:modelValue', content);
+    emit('content-changed', content);
+    immediateSave();
+    updateToolbar();
+  });
+}
+
 // 切换图表面板下拉框显示状态
 function toggleChartPanelDropdown() {
   showChartPanelDropdown.value = !showChartPanelDropdown.value;
@@ -1549,13 +1732,13 @@ function calculateChartPanelPosition() {
   cursor: pointer;
   outline: none;
   appearance: none;
-  background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2'%3e%3cpath d='M6 9l6 6 6-6'/%3e%3c/svg%3e");
+  background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.o  rg/2000/svg' viewBox='0 0 24 24' fill='none' strok  e='%23666' stroke-width='2'%3e%3cpath d='M6 9l6 6 6-6'/%3e%3c/svg%3e");
   background-repeat: no-repeat;
   background-position: right 8px center;
-  background-size: 12px;
+  background-size: 1 2px;
   padding-right: 30px;
   min-width: 90px;
-  box-sizing: border-box;
+  box-sizing: bord er-box;
 }
 
 .toolbar select:hover {
@@ -1597,14 +1780,14 @@ function calculateChartPanelPosition() {
 /* ========== Mention 样式 ==========
 .floating-dropdown {
   position: absolute;
-  z-index: 1000;
+  z-index: 1000    ;
   background: white;
   border: 1px solid #d9d9d9;
   border-radius: 4px;
   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
   min-width: 120px;
   max-height: 200px;
-  overflow-y: auto;
+  overfl      ow-y: auto;
 }
 
 /* 指标高亮样式 */
@@ -1641,7 +1824,7 @@ function calculateChartPanelPosition() {
   background: white;
   border: 1px solid #ccc;
   border-radius: 4px;
-  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2  );
   min-width: 100px;
 }
 

+ 110 - 12
src/modules/report-template/editor-model/EditorTitleHeader.vue

@@ -7,6 +7,7 @@
             v-if="is_edit"
             :disabled="is_loading_title"
             v-model="edit_title"
+            :max-length="50"
             style="width: 200px; margin: 0 8px"
           ></a-input>
           <icon-check v-if="!is_loading_title" class="icon" @click="handleSaveTitle" />
@@ -30,7 +31,9 @@
       <a-button class="btn-save" v-else :disabled="is_loading_template"
         ><icon-loading class="btn-icon" /> &nbsp;保存中...
       </a-button>
-      <a-button class="btn-report"> <img class="btn-icon" :src="SendIcon" alt="" />&nbsp;生成报告 </a-button>
+      <a-button class="btn-report" @click="handleSendReport">
+        <img class="btn-icon" :src="SendIcon" alt="" />&nbsp;生成报告
+      </a-button>
     </div>
   </div>
   <SaveTemplateModel
@@ -39,8 +42,14 @@
     :template_name="title"
     @submit="handleSave"
     @close="handleClose"
+    @save-error="handleSaveError"
   ></SaveTemplateModel>
   <SaveTipsModel v-model:visible="show_save_Tips" @ok="toSaveAsTemplate" @cancel="toNotSaveAsTemplate"></SaveTipsModel>
+  <SaveErrorModal
+    v-model:visible="show_save_error"
+    :complete_list="save_error_list"
+    @ok="handleSaveErrorOk"
+  ></SaveErrorModal>
 </template>
 
 <script setup >
@@ -52,6 +61,8 @@ import SaveTipsModel from './SaveTipsModel.vue';
 import { editTemplateItem, addTemplate } from '../../conversation-dialog/api-template';
 import { Message } from '@arco-design/web-vue';
 import { useReportEditor } from '../../../base/store/use-report-editor';
+import { Modal } from '@arco-design/web-vue';
+import SaveErrorModal from './SaveErrorModal.vue';
 
 const reportEditor = useReportEditor();
 
@@ -59,23 +70,30 @@ const props = defineProps({
   template_id: String,
   draft_id: String,
   template_name: String,
-  template_save_time: String
+  template_save_time: String,
+  common_config: Object
 });
-const emit = defineEmits(['save_template', 'content-changed', 'update-draft']);
+const emit = defineEmits(['save_template', 'content-changed', 'update-draft', 'send-report']);
 
-const title = ref('草稿-' + Date.now());
+const title = ref((props.common_config?.theme || '') + '_' + Date.now());
+// const title = ref(props.common_config.theme || '' + Date.now());
 const edit_title = ref('');
 const time = ref('10:20');
 const is_edit = ref(false);
 const is_loading_title = ref(false);
 const show_save_Tips = ref(false);
+const save_error_list = ref([]);
 const is_loading_template = ref(false);
 
 const show_save_template_model = ref(false);
+const show_save_error = ref(false);
 
 const is_need_save = computed(() => {
   return reportEditor.QueryIsNeedSave;
 });
+// const is_need_save_template = computed(() => {
+//   return reportEditor.QueryIsNeedSaveTemplate;
+// });
 
 const handleEdit = () => {
   edit_title.value = title.value;
@@ -96,7 +114,8 @@ const handleSaveTitle = async () => {
         title.value = edit_title.value;
         Message.success('编辑成功');
       } else {
-        Message.error('编辑失败');
+        console.log(res, 789);
+        Message.error(res.msg);
       }
     } catch (error) {
       Message.error('编辑失败');
@@ -126,6 +145,16 @@ const handleSave = (data) => {
     ...data
   });
 };
+const handleSaveError = (list) => {
+  show_save_template_model.value = false;
+  show_save_error.value = true;
+  save_error_list.value = list;
+  console.log(list);
+};
+const handleSaveErrorOk = () => {
+  show_save_error.value = false;
+  save_error_list.value = [];
+};
 const toNotSaveAsTemplate = () => {
   show_save_Tips.value = false;
 };
@@ -138,9 +167,19 @@ const formatTime = (updated_at) => {
 const ToSave = async () => {
   // 如果存在id,则保存新的修改到当前模板中
   if (props.template_id) {
+    // 检查指标配置是否完善
+    const complete_list = reportEditor.checkMetricConfigComplete();
+    if (complete_list.length > 0) {
+      // Message.error('指标配置不完善,请完善后再保存');
+      save_error_list.value = complete_list;
+      show_save_error.value = true;
+      return;
+    }
     const res_temp = await editTemplateFunc(props.template_id);
     if (res_temp.code / 1 === 200) {
       Message.success('模板保存成功');
+      reportEditor.setIsNeedSave(false);
+      reportEditor.setIsNeedSaveTemplate(false);
     } else {
       Message.error('模板保存失败');
     }
@@ -148,7 +187,7 @@ const ToSave = async () => {
   }
   // 如果不存在id,则保存草稿
   if (props.draft_id) {
-    const res_draft = await editTemplateFunc(props.draft_id);
+    const res_draft = await editTemplateFunc(props.draft_id, 2);
     if (res_draft.code / 1 === 200) {
       Message.success('草稿保存成功');
     } else {
@@ -158,7 +197,7 @@ const ToSave = async () => {
   }
   if (!props.template_id && !props.draft_id) {
     const res = await addTemplateFunc(2);
-    if (res.data.id) {
+    if (res.data?.id) {
       Message.success('保存草稿成功');
       emit('update-draft', {
         id: res.data.id,
@@ -166,14 +205,56 @@ const ToSave = async () => {
         created_at: res.data.created_at
       });
     } else {
-      Message.error('保存草稿失败');
+      Message.error(res.msg);
     }
   }
   // 不存在id,则提醒是否保存成模板
   // show_save_Tips.value = true;
 };
 
-const editTemplateFunc = async (id) => {
+const handleSendReport = async () => {
+  if (!props.template_id && !props.draft_id) {
+    return Message.error('请先保存后,再生成报告');
+  }
+  if (props.template_id) {
+    const complete_list = reportEditor.checkMetricConfigComplete();
+    if (complete_list.length > 0) {
+      // Message.error('指标配置不完善,请完善后再保存');
+      save_error_list.value = complete_list;
+      show_save_error.value = true;
+      return;
+    }
+    if (is_need_save.value) {
+      const res_temp = await editTemplateFunc(props.template_id);
+      if (res_temp.code / 1 === 200) {
+        reportEditor.setIsNeedSave(false);
+        console.log('去生成报告', res_temp.data.id, res_temp.data.name);
+        emit('send-report', { id: res_temp.data.id, name: res_temp.data.name });
+      } else {
+        Message.error('模板保存失败,请稍候重试');
+      }
+      return;
+    } else {
+      console.log('去生成报告', { id: props.template_id, name: title.value });
+      emit('send-report', { id: props.template_id, name: title.value });
+      return;
+    }
+  }
+  if (props.draft_id) {
+    const complete_list = reportEditor.checkMetricConfigComplete();
+    if (complete_list.length > 0) {
+      // Message.error('指标配置不完善,请完善后再保存');
+      save_error_list.value = complete_list;
+      show_save_error.value = true;
+      return;
+    }
+
+    console.log('去生成报告', { id: props.draft_id, name: title.value });
+    emit('send-report', { id: props.draft_id, name: title.value });
+  }
+};
+
+const editTemplateFunc = async (id, type = 1) => {
   try {
     is_loading_template.value = true;
     const res = await editTemplateItem({
@@ -181,12 +262,13 @@ const editTemplateFunc = async (id) => {
       name: title.value,
       content: reportEditor.target_report_markdown,
       template_config: JSON.stringify(reportEditor.metrics_list),
-      common_config: JSON.stringify(reportEditor.common_config)
+      common_config: JSON.stringify(reportEditor.common_config),
+      type: type
     });
     if (res.code / 1 === 200) {
       return res;
     } else {
-      Message.error('保存失败');
+      Message.error(res.msg);
     }
   } catch (error) {
     Message.error('保存失败');
@@ -223,6 +305,20 @@ const addTemplateFunc = async (type = 1) => {
   }
 };
 
+onMounted(() => {});
+watch(
+  () => reportEditor.getTargetMd,
+  (newVal) => {
+    if (newVal) {
+      if (!props.template_id && !props.draft_id) {
+        ToSave();
+      }
+    }
+  },
+  {
+    immediate: true
+  }
+);
 watch(
   () => props.template_id,
   (newVal, oldVal) => {
@@ -260,11 +356,13 @@ watch(
             created_at: res.data.created_at
           });
           reportEditor.setIsNeedSave(false);
+        } else {
+          Message.error(res.msg);
         }
       }
       if (!props.template_id && props.draft_id) {
         console.log(props.draft_id, 99999999999999);
-        const res = await editTemplateFunc(props.draft_id);
+        const res = await editTemplateFunc(props.draft_id, 2);
         if (res.code / 1 === 200) {
           emit('update-draft', {
             id: res.data.id,

+ 217 - 0
src/modules/report-template/editor-model/SaveErrorModal.vue

@@ -0,0 +1,217 @@
+<template>
+  <a-modal v-model:visible="visible" :simple="true" :hideCancel="false" width="600px">
+    <template #title>
+      <div class="title-box">
+        <icon-exclamation-circle-fill class="title-icon" />
+        <span class="title-text">度量配置不完善</span>
+      </div>
+    </template>
+
+    <div class="body-box">
+      <div v-if="complete_list && complete_list.length > 0" class="error-list">
+        <div class="error-header">
+          <span class="header-text">以下度量过滤器需要完善:</span>
+          <span class="error-count">共 {{ complete_list.length }} 项</span>
+        </div>
+
+        <div class="error-items">
+          <div class="body-item" v-for="(item, index) in complete_list" :key="index">
+            <div class="item-row">
+              <span class="item-label">度量名称:</span>
+              <span class="item-value">{{ item.metric_name || '未命名' }}</span>
+            </div>
+            <div class="item-row">
+              <span class="item-label">过滤器名称:</span>
+              <span class="item-value">{{ item.filter_name || '无' }}</span>
+            </div>
+            <div class="item-row">
+              <span class="item-label">所属数据源:</span>
+              <span class="item-value">{{ item.source_name || '无' }}</span>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div v-else class="empty-state">
+        <icon-check-circle-fill class="empty-icon" />
+        <span class="empty-text">所有指标配置已完成</span>
+      </div>
+    </div>
+
+    <template #footer>
+      <a-button class="update_btn" @click="handleOk">我知道了</a-button>
+    </template>
+  </a-modal>
+</template>
+
+<script setup>
+import { toRefs } from 'vue';
+
+const props = defineProps({
+  visible: Boolean,
+  loading: Boolean,
+  complete_list: Array
+});
+
+const { visible, loading, complete_list } = toRefs(props);
+const emit = defineEmits(['ok', 'cancel']);
+
+const handleOk = () => {
+  emit('ok');
+};
+</script>
+
+<style scoped>
+.title-box {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.title-icon {
+  color: #ff4d4f;
+  font-size: 22px;
+  margin-right: 10px;
+}
+
+.title-text {
+  font-size: 16px;
+  font-weight: 600;
+  color: #1f2329;
+}
+
+.body-box {
+  padding: 8px 0;
+}
+
+.error-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.error-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding-bottom: 12px;
+  border-bottom: 1px solid #e5e6eb;
+}
+
+.header-text {
+  font-size: 14px;
+  color: #4e5969;
+}
+
+.error-count {
+  font-size: 14px;
+  color: #ff4d4f;
+  font-weight: 500;
+  background-color: #fff2f0;
+  padding: 4px 12px;
+  border-radius: 12px;
+}
+
+.error-items {
+  max-height: 400px;
+  overflow-y: auto;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  padding-right: 8px;
+}
+
+.error-items::-webkit-scrollbar {
+  width: 6px;
+}
+
+.error-items::-webkit-scrollbar-thumb {
+  background-color: #c9cdd4;
+  border-radius: 3px;
+}
+
+.error-items::-webkit-scrollbar-track {
+  background-color: #f2f3f5;
+  border-radius: 3px;
+}
+
+.body-item {
+  background-color: #f7f8fa;
+  border: 1px solid #e5e6eb;
+  border-radius: 8px;
+  padding: 12px 16px;
+  transition: all 0.2s ease;
+}
+
+.body-item:hover {
+  background-color: #f2f3f5;
+  border-color: #c9cdd4;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+}
+
+.item-row {
+  display: flex;
+  align-items: center;
+  margin-bottom: 8px;
+}
+
+.item-row:last-child {
+  margin-bottom: 0;
+}
+
+.item-label {
+  font-size: 13px;
+  color: #86909c;
+  min-width: 80px;
+  font-weight: 500;
+}
+
+.item-value {
+  font-size: 13px;
+  color: #1f2329;
+  font-weight: 400;
+  word-break: break-all;
+}
+
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 40px 20px;
+  gap: 12px;
+}
+
+.empty-icon {
+  font-size: 48px;
+  color: #00b42a;
+}
+
+.empty-text {
+  font-size: 14px;
+  color: #4e5969;
+}
+
+.update_btn {
+  background-color: var(--primary-default);
+  color: #fff;
+  min-width: 100px;
+  height: 32px;
+  font-size: 14px;
+  border-radius: 4px;
+  transition: all 0.2s ease;
+}
+
+.update_btn:hover {
+  background-color: var(--primary-default);
+  color: #fff;
+  opacity: 0.8;
+  transform: translateY(-1px);
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+}
+
+.update_btn:active {
+  transform: translateY(0);
+}
+</style>
+

+ 13 - 1
src/modules/report-template/editor-model/SaveTemplateModel.vue

@@ -75,6 +75,7 @@ import { getTemplateGroupList, addTemplateGroup, addTemplate, editTemplateItem }
 import { useReportEditor } from '../../../base/store/use-report-editor';
 import { getUserName } from '../../../utils/get-user-name';
 
+
 const reportEditor = useReportEditor();
 
 const props = defineProps({
@@ -84,7 +85,7 @@ const props = defineProps({
   template_id: String,
 });
 
-const emit = defineEmits(['submit', 'close']);
+const emit = defineEmits(['submit', 'close','save-error']);
 
 const visible = computed(() => props.show_model);
 const templateGroups = ref([]);
@@ -152,7 +153,14 @@ const handleSave = async () => {
     Message.error('模板内容不能为空');
     return;
   }
+  // 检查指标配置是否完善
+  const complete_list = reportEditor.checkMetricConfigComplete()
+  if (complete_list.length > 0) {
+    // Message.error('指标配置不完善,请完善后再保存');
+    emit('save-error',complete_list)
 
+    return;
+  }
   is_loading.value = true;
   try {
     const saveData = {
@@ -182,8 +190,12 @@ const handleSave = async () => {
     if (res.code / 1 === 200) {
       console.log(form.value.template_config,'配置')
       Message.success(props.template_id ? '模板更新成功' : '模板保存成功');
+      reportEditor.setIsNeedSave(false);
+      reportEditor.setIsNeedSaveTemplate(false);
       emit('submit',res.data);
       clearForm();
+    }else {
+      Message.error(res.msg)
     }
   } catch (error) {
     Message.error(props.template_id ? '模板更新失败' : '模板保存失败');

+ 86 - 4
src/modules/report-template/filter-model/MentionFilter.vue

@@ -45,9 +45,16 @@
           <!-- <div class=""></div> -->
         </div>
       </div>
-      <!-- <div class="update">
-        <a-button type="primary" class="btn-update" @click="handleUpdate">更新</a-button>
-      </div> -->
+      <div class="filter-config">
+        <div class="title">全局取消继承</div>
+        <div class="filter-config-body">
+          <div class="filter-config-body-item" v-for="item in common_config_list" :key="item.field_key">
+            <a-checkbox v-model="item.is_checked" @change="(event) => handleCommonConfigClick(item, event)">{{
+              item.field_name
+            }}</a-checkbox>
+          </div>
+        </div>
+      </div>
     </div>
   </div>
   <StringFilter
@@ -97,6 +104,10 @@ const props = defineProps({
   filter_option: {
     type: Object,
     default: () => {}
+  },
+  target_common_config_info: {
+    type: Array,
+    default: () => []
   }
 });
 
@@ -122,6 +133,8 @@ const filter_form_data = ref(null);
 
 const filter_id = ref(''); // 添加的每个过滤器对应的唯一ID
 
+const common_config_list = ref([]);
+
 const onDragOver = (event) => {
   event.preventDefault();
   event.stopPropagation();
@@ -328,6 +341,16 @@ const handleUpdate = () => {
     filter_form_data.value = null;
   }
 };
+
+const handleCommonConfigClick = (item) => {
+  common_config_list.value.forEach((it) => {
+    if (it.field_key === item.field_key) {
+      it.is_extend = item.is_checked ? 0 : 1;
+      it.is_checked = !item.is_checked;
+    }
+  });
+  reportEditor.changeCommonConfigListConfig(common_config_list.value);
+};
 watch(
   [() => props.filter_option, () => reportEditor.QueryTargetDimensionListConfig],
   ([newVal, newTargetDiemensionList]) => {
@@ -378,6 +401,48 @@ watch(
     deep: true
   }
 );
+
+watch(
+  () => props.target_common_config_info,
+  (newVal, oldVal) => {
+    const targetExtendCommonConfigInfo = reportEditor.HasTargetExtendCommonConfigInfo;
+    if (targetExtendCommonConfigInfo) {
+      return;
+    }
+    common_config_list.value = newVal.map((item) => ({
+      ...item,
+      is_checked: item.is_extend / 1 === 1 ? false : true // 0: 不继承,选中,1: 继承,不选中
+    }));
+    const submit_common_config_list = common_config_list.value.map((item) => ({
+      ...item,
+      is_extend: item.is_checked ? 0 : 1
+    }));
+    console.log('common_config_list', common_config_list.value);
+    reportEditor.changeCommonConfigListConfig(submit_common_config_list);
+  },
+  {
+    immediate: true,
+    deep: true
+  }
+);
+
+watch(
+  [() => props.filter_option, () => reportEditor.QueryTargetExtendCommonConfigInfo],
+  ([newVal, newTargetExtendCommonConfigInfo]) => {
+    console.log(1111111111111111111, newTargetExtendCommonConfigInfo);
+    // 确保每个item都有is_checked属性
+    const processedConfig = newTargetExtendCommonConfigInfo.map((item) => ({
+      ...item,
+      is_checked: item.is_extend / 1 === 1 ? false : true // 0: 不继承,选中,1: 继承,不选中
+    }));
+    common_config_list.value = [...processedConfig];
+  },
+  {
+    immediate: true,
+    deep: true
+  }
+);
+
 onMounted(() => {});
 defineExpose({
   saveDataToStore: handleUpdate
@@ -473,7 +538,21 @@ defineExpose({
   color: #275fd4;
 }
 .filter {
-  height: calc(100% - 100px - 240px - 20px - 30px);
+  height: calc(100% - 100px - 240px - 30px - 200px);
+}
+.filter-config {
+  height: 200px;
+  padding: 10px;
+  box-sizing: border-box;
+}
+.filter-config-body {
+  height: calc(100% - 20px);
+  overflow-y: auto;
+  padding: 10px 5px;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  gap: 5px;
 }
 .filter-list {
   display: flex;
@@ -512,6 +591,9 @@ defineExpose({
 .empty-tip {
   text-align: center;
 }
+:deep(.arco-checkbox-checked .arco-checkbox-icon) {
+  background-color: var(--primary-default);
+}
 /* .update {
 
 } */

+ 1 - 1
src/modules/report-template/filter-model/model/DualListSelector.vue

@@ -145,7 +145,7 @@ const getRegionEnumFun = async () => {
 
   try {
     const res = await getRegionEnum({
-      enum_type_id: str(enumTypeId)
+      enum_type_id: String(enumTypeId)
     });
     console.log('API response:', res);
 

+ 4 - 0
src/modules/report-template/source-model/DataSource.vue

@@ -38,7 +38,11 @@ const handleChange = (value) => {
 watch(
   () => props.target_source_id,
   (newVal, oldVal) => {
+    console.log(newVal, 9999);
     handleChange(newVal);
+  },
+  {
+    immediate: true
   }
 );
 // 当数据源列表变化时,默认选择第一个

+ 10 - 2
src/modules/report-template/template-detail/EditorModel.vue

@@ -5,14 +5,17 @@
       :draft_id="draft_id"
       :template_name="template_name"
       :template_save_time="template_save_time"
+      :common_config="common_config"
       @save_template="handleSaveTemplate"
       @content-changed="handleContentChanged"
       @update-draft="handleUpdateDraft"
+      @send-report="handleSendReport"
     ></EditorTitleHeader>
     <EditorDetail
       :modelValue="render_html"
       :metrics_list="metrics_list"
       :extracted_metrics="extracted_metrics"
+      :common_config="common_config"
       @mention-click="handleMentionClick"
       @chart-click="handleChartClick"
       @close-filter-model="closeFilterModel"
@@ -39,7 +42,8 @@ const props = defineProps({
   draft_id: String,
   template_name: String,
   template_save_time: String,
-  is_loading_template_detail: Boolean
+  is_loading_template_detail: Boolean,
+  common_config: Object
 });
 
 const emit = defineEmits([
@@ -50,7 +54,8 @@ const emit = defineEmits([
   'submit-html',
   'save-template',
   'content-changed',
-  'update-draft'
+  'update-draft',
+  'send-report'
 ]);
 
 const handleClick = () => {
@@ -84,6 +89,9 @@ const handleContentChanged = (html) => {
 const handleUpdateDraft = (data) => {
   emit('update-draft', data);
 };
+const handleSendReport = (data) => {
+  emit('send-report', data);
+};
 
 function getFirstH1Content(htmlString) {
   // 创建临时DOM元素用于解析HTML

+ 5 - 0
src/modules/report-template/template-detail/FilterModel.vue

@@ -4,6 +4,7 @@
       ref="mentionFilterRef"
       v-if="type == 'mention'"
       :filter_option="filter_option"
+      :target_common_config_info="target_common_config_info"
       @content-changed="handleContentChanged"
     ></MentionFilter>
   </div>
@@ -21,6 +22,10 @@ const props = defineProps({
   filter_option: {
     type: Object,
     default: () => {}
+  },
+  target_common_config_info: {
+    type: Array,
+    default: () => []
   }
 });
 const emit = defineEmits(['content-changed']);

+ 1 - 1
src/modules/template-manage/template/AddTemplate.vue

@@ -4,7 +4,7 @@
     <div>
       <a-form :model="form">
         <a-form-item field="name" label="模板名称" required validate-trigger="input">
-          <a-input v-model="form.name" placeholder="名称" />
+          <a-input v-model="form.name" :max-length="50" placeholder="名称" />
         </a-form-item>
         <a-form-item field="description" label="模板描述">
           <a-textarea

+ 1 - 1
src/modules/template-manage/template/TemplateDetailHeader.vue

@@ -61,7 +61,7 @@ const clearSearch = () => {
   // })
 }
 const handleSearch = funcDebounce(() => {
-  console.log(111111111)
+  // console.log(111111111)
   emit('toSearch', {
     type: search_type.value,
     value: search_value.value

+ 105 - 26
src/views/conversation/ConversationChatContainer.vue

@@ -51,6 +51,7 @@
         @stop="stopResponse"
         :is_input_ing="input_ing"
         @changeFooterStyle="changeFooterStyle"
+        @changeFooterStyleForOutline="changeFooterStyleForOutline"
         @open_template_manage="toTemplateManage"
         @edit-template="handleEditTemplate"
         :is_merge_table="is_merge_table"
@@ -63,7 +64,9 @@
         ref="template"
         :target_template_id="target_template_id"
         :target_outline_md="target_outline_md"
+        :target_common_config="target_common_config"
         @toBackHome="closeTemplateEditor"
+        @send-report="handleSendReport"
       ></TemplateEditor>
     </div>
     <div class="template" v-if="show_template_manage">
@@ -112,7 +115,8 @@ import {
   getDialogSessionId_AL,
   getAgentIcons,
   updateAgentIcons,
-  downloadReport
+  downloadReport,
+  uploadForOutline
 } from '../../modules/conversation-dialog/api-dialog';
 
 import { mergeTableData, getCheckedErrorList } from '../../modules/table-dialog/api-dialog';
@@ -165,6 +169,7 @@ const show_template_manage = ref(false);
 
 const target_template_id = ref('');
 const target_outline_md = ref('');
+const target_common_config = ref(null);
 
 const dialog_input = ref(null);
 
@@ -184,18 +189,18 @@ const scrollState = ref({
 
 // 聊天对话列表
 const moke_data = ref([
-  {
-    client_custom_item_type: 'q',
-    question: '帮我撰写一份报告,生成关于xxxxxxxx的报告大纲',
-    client_custom_chat_status: 2
-  },
-  {
-    client_custom_item_type: 'a',
-    answer: str_md,
-    original_answer: str_md,
-    client_custom_report_outline: 1,
-    client_custom_chat_status: 2
-  }
+  // {
+  //   client_custom_item_type: 'q',
+  //   question: '帮我撰写一份报告,生成关于xxxxxxxx的报告大纲',
+  //   client_custom_chat_status: 2
+  // },
+  // {
+  //   client_custom_item_type: 'a',
+  //   answer: str_md,
+  //   original_answer: str_md,
+  //   client_custom_report_outline: 1,
+  //   client_custom_chat_status: 2
+  // }
 ]);
 // 历史记录参数
 const history_item_params = ref({
@@ -285,9 +290,15 @@ const main_conversation_container = computed(() => {
         };
       }
     } else {
-      return {
-        height: 'calc(100% - 160px - 100px - 25px - 20px)'
-      };
+      if (is_change_footer_height.value) {
+        return {
+          height: 'calc(100% - 160px - 100px - 25px - 20px - 80px)'
+        };
+      } else {
+        return {
+          height: 'calc(100% - 160px - 100px - 25px - 20px)'
+        };
+      }
     }
   } else {
     if (is_change_footer_height.value) {
@@ -413,6 +424,9 @@ const scroll2ListTop = () => {
 // 展示停止问答按钮
 const show_stop_sse = ref(false);
 
+// 大纲中的公共配置
+let common_config = {};
+
 // 初始化sse相关方法
 const { startStream, stopStream } = useFetchEventSource();
 const { startReportStream, stopReportStream } = useFetchEventSourceReport();
@@ -431,6 +445,7 @@ const convertToLowerCase = (str) => {
 // 发送对话
 const handleSubmit = async (options) => {
   console.log(isElectron(), '环境');
+  console.log(options, 999);
   let ask_text = options.text; // 文本
   let ask_files = options.files || [];
   let event_id = options.event_id; //表格处理专用
@@ -537,6 +552,36 @@ const handleSubmit = async (options) => {
         return;
       }
     }
+    let file_id = '';
+    let file_path = '';
+
+    // 如果有文件说明时大纲的模板文件
+    if (ask_files.length) {
+      try {
+        const file = ask_files[0].origin_file;
+        // console.log(file);
+        const form_data = new FormData();
+        form_data.append('file', file);
+        const res = await uploadForOutlineFun(form_data);
+        console.log(res);
+        if (res.code / 1 == 200) {
+          file_id = res.data.file_id;
+          file_path = res.data.file_path;
+        } else {
+          handleChatRequestionException({
+            message: `code: ${res.code} message: ${res.msg}`
+          });
+          return;
+        }
+      } catch (e) {
+        handleChatRequestionException({
+          message: `code: ${500} message: 上传大纲模板接口异常`
+        });
+        return;
+      }
+    }
+    common_config = {};
+
     agentUsedStore.updateAgentUsed('bgsc');
     const username = await getUserName();
     const query_params = {
@@ -546,8 +591,12 @@ const handleSubmit = async (options) => {
       app_id: window.APP_ID_AL,
       template_id: template_id
     };
+    if (file_id) {
+      query_params.word_file_id = file_id;
+    }
     // 默认每次生成报告时都不是大纲,当内容生成后才能判断是否是大纲
     is_outline.value = false;
+
     startReportStream(query_params, sseOnMessageReport);
     return;
     // endIf
@@ -601,6 +650,11 @@ const handleSubmit = async (options) => {
   }
 };
 
+// 上传大纲模板
+const uploadForOutlineFun = async (data) => {
+  return await uploadForOutline(data);
+};
+
 /**
  * 处理sse返回内容的函数
  *
@@ -720,6 +774,7 @@ const processSingleMessage = async (data) => {
   let is_end;
   // let is_download;
   let res_attachments;
+  // let common_config = {};
 
   try {
     const res_echart = processInputString(data);
@@ -731,6 +786,13 @@ const processSingleMessage = async (data) => {
       if (res.metadata && res.metadata.report_type && res.metadata.report_type / 1 === 3) {
         is_outline.value = true;
       }
+      if (res.metadata && Object.keys(res.metadata).length) {
+        common_config['start_date'] = res.metadata.date?.start_date || '';
+        common_config['end_date'] = res.metadata.date?.end_date || '';
+        common_config['area'] = res.metadata?.area || '';
+        common_config['theme'] = res.metadata?.theme || '';
+        console.log('res.metadata', res.metadata && Object.keys(res.metadata).length, common_config);
+      }
       if (is_outline.value) {
         if (!res.content) {
           res.content = [{ text: { value: '' }, type: 'text' }];
@@ -738,7 +800,7 @@ const processSingleMessage = async (data) => {
         }
         if (Array.isArray(res.content)) {
           let res_content = res.content[0];
-          console.log(res_content, 1);
+          // console.log(res_content, 1);
           if (!res_content) {
             res_content = { text: { value: '##' }, type: 'text' };
           }
@@ -760,7 +822,7 @@ const processSingleMessage = async (data) => {
   }
 
   const last_msg = chat_new_list.value[chat_new_list.value.length - 1];
-  console.log(res_data, 909999);
+  // console.log(res_data, 909999);
   const currentChat = formatDialogAnswerFromSseReceiveMessageReport(res_data, last_msg, res_attachments, is_end);
   // console.log(currentChat, 212121);
   const chat_status = currentChat.status;
@@ -770,6 +832,12 @@ const processSingleMessage = async (data) => {
     const chat_data = currentChat.data;
     if (chat_type.value === 'bgsc' && is_outline.value) {
       chat_data.client_custom_report_outline = true;
+      if (Object.keys(common_config).length) {
+        chat_data.client_custom_report_outline_start_date = common_config?.start_date;
+        chat_data.client_custom_report_outline_end_date = common_config?.end_date;
+        chat_data.client_custom_report_outline_area = common_config?.area;
+        chat_data.client_custom_report_outline_theme = common_config?.theme;
+      }
     }
     // 报告生成相关处理
     if (chat_type.value === 'bgsc' && is_end && !is_outline.value) {
@@ -1004,9 +1072,9 @@ let markdown_chart = null;
 const sseOnMessageZnws = (event) => {
   const res_data = JSON.parse(event.data);
   // let markdown_chart = null;
-  console.log(res_data);
+  // console.log(res_data);
   if (res_data.metadata && res_data.metadata.markdown_chart) {
-    console.log(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) {
@@ -1299,15 +1367,15 @@ const getDialogHistoryDetailFun = async () => {
     let detail_list;
     if (chat_type.value == 'znwd') {
       detail_list = formatDialogHistoryMessages(res.data.rows, '1');
-      BD_session.value.conversation_id = res.data.rows[0].conversation_id;
+      BD_session.value.conversation_id = res.data.rows[0]?.conversation_id;
       history_item_params.value.total = res.data.total;
     } else if (chat_type.value == 'bgsc') {
       detail_list = formatDialogHistoryMessages(res.data.rows, '2');
-      AL_session.value.conversation_id = res.data.rows[0].conversation_id;
+      AL_session.value.conversation_id = res.data.rows[0]?.conversation_id;
       history_item_params.value.total = res.data.total;
     } else if (chat_type.value == 'znws') {
       detail_list = formatDialogHistoryMessages(res.data.rows, '3');
-      WS_session.value.conversation_id = res.data.rows[0].conversation_id;
+      WS_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');
@@ -1427,7 +1495,7 @@ const updateScrollState = () => {
   scrollState.value.scrollHeight = list_scroller.value.scrollHeight;
   scrollState.value.clientHeight = list_scroller.value.clientHeight;
   scrollState.value.scrollTop = list_scroller.value.scrollTop;
-  console.log(scrollState.value, 9900);
+  // console.log(scrollState.value, 9900);
 };
 // 当前智能体的头像
 const target_agent_icon = ref('');
@@ -1613,7 +1681,7 @@ const handleEditTemplate = (record) => {
   show_template_editor.value = true;
   show_template_manage.value = false;
   target_template_id.value = record.id;
-  console.log(record, '9090');
+  // console.log(record, '9090');
 };
 const manageToBack = () => {
   show_template_manage.value = false;
@@ -1623,9 +1691,20 @@ const closeTemplateEditor = () => {
   show_template_editor.value = false;
   target_template_id.value = '';
   target_outline_md.value = '';
+  target_common_config.value = null;
 };
-const toUpdateOutLine = (md) => {
+
+const handleSendReport = (data) => {
+  show_template_editor.value = false;
+  handleSubmit({ text: `请根据 ${data.name} 生成报告`, template_id: data.id });
+};
+
+const toUpdateOutLine = (data) => {
+  // console.log(222, data);
   show_template_editor.value = true;
+  let md = data.md_content;
+  target_common_config.value = data;
+  console.log(md);
   target_outline_md.value = replaceMdUids(md || '');
 };
 const toConfirmOutLine = () => {

+ 2 - 0
src/views/report/ReportDetail.vue

@@ -227,6 +227,8 @@ const saveHtmlFunc = async () => {
   clearTimeout(timeout_interval);
   timeout_interval = null;
   if (save_status.value / 1 == 2) return;
+  console.log(my_editor.value?.getHtml(),111)
+  if (!my_editor.value?.getHtml()) return;
   save_status.value = 1;
   const data = {
     id: id.value,