Explorar o código

feat:灵活模板新增同环比配置,新增数据源管理

韩洋 hai 6 meses
pai
achega
be4c417320
Modificáronse 35 ficheiros con 2682 adicións e 1532 borrados
  1. 11 2
      src/base/store/use-report-editor.js
  2. 19 0
      src/modules/conversation-chat/DialogInput.vue
  3. 123 0
      src/modules/conversation-chat/dialog-input/DataSourceManage.vue
  4. 10 1
      src/modules/conversation-chat/dialog-input/InputActions.vue
  5. 88 0
      src/modules/conversation-dialog/api-template.js
  6. 115 0
      src/modules/data-source-manage/DataSourceModal.vue
  7. 34 0
      src/modules/data-source-manage/MangeHeader.vue
  8. 43 0
      src/modules/data-source-manage/add-source/AddSourceHeader.vue
  9. 309 0
      src/modules/data-source-manage/add-source/AddSourceModel.vue
  10. 183 0
      src/modules/data-source-manage/add-source/components/ApiConfig.vue
  11. 213 0
      src/modules/data-source-manage/add-source/components/ApiFieldConfig.vue
  12. 127 0
      src/modules/data-source-manage/add-source/components/BasicInfo.vue
  13. 111 0
      src/modules/data-source-manage/add-source/components/DbConfig.vue
  14. 194 0
      src/modules/data-source-manage/add-source/components/DbFieldConfig.vue
  15. 64 0
      src/modules/data-source-manage/add-source/参数格式.txt
  16. 402 0
      src/modules/data-source-manage/data-source/DataSourceBody.vue
  17. 111 0
      src/modules/data-source-manage/data-source/DataSourceHeader.vue
  18. 15 0
      src/modules/report-template/TemplateDetail.vue
  19. 1 1
      src/modules/report-template/TemplateEditor.vue
  20. 1 0
      src/modules/report-template/editor-chart/TableChart.vue
  21. 3 1
      src/modules/report-template/editor-model/CommonDimension.vue
  22. 0 1453
      src/modules/report-template/editor-model/EditorDetail - 副本.vue
  23. 7 7
      src/modules/report-template/editor-model/EditorDetail.vue
  24. 192 10
      src/modules/report-template/editor-model/EditorDetail_备份_无同环比.vue
  25. 202 38
      src/modules/report-template/editor-select/MentionActionMenu.vue
  26. 0 0
      src/modules/report-template/editor-select_备份_无同环比/ChartPanelDropdown.vue
  27. 0 0
      src/modules/report-template/editor-select_备份_无同环比/CustomModal.vue
  28. 11 4
      src/modules/report-template/editor-select_备份_无同环比/MentionActionMenu.vue
  29. 8 9
      src/modules/report-template/editor-select_备份_无同环比/MentionDropdown.vue
  30. 30 0
      src/modules/report-template/filter-model/ChartFilter.vue
  31. 24 5
      src/modules/report-template/filter-model/MentionFilter.vue
  32. 1 0
      src/modules/report-template/template-detail/EditorModel.vue
  33. 8 0
      src/modules/report-template/template-detail/FilterModel.vue
  34. 1 1
      src/modules/template-manage/template/AddTemplate.vue
  35. 21 0
      src/views/About.vue

+ 11 - 2
src/base/store/use-report-editor.js

@@ -149,6 +149,8 @@ export const useReportEditor = defineStore('report-editor', {
           "sort_field_key": '', // 配置排序字段key,比如接口返回数据为列表,每个元素为字典,需要根据字典中的age字段排序,则配置为age
           "default_metric_field_key": '', // 默认指标字段key, 如果指标配置中没有配置指标字段key,则默认使用默认指标字段key
           "format_field_key": '', // 指标字段key
+          "yoy_or_mom": '', /// 同比环比
+          "aggregation_type": 'sum', /// 聚合方式
         }
       }
 
@@ -177,6 +179,10 @@ export const useReportEditor = defineStore('report-editor', {
       this.metrics_list.forEach(item => {
         if (item.index === this.target_metric_id) {
           item.metric_config[config.name] = config.value
+          // 当聚合方式发生变化时,同时更新对应的 aggs_config 中的 aggs_type
+          if (config.name === 'aggregation_type' && item.aggs_config) {
+            item.aggs_config.aggs_type = config.value
+          }
         }
       })
     },
@@ -194,7 +200,7 @@ export const useReportEditor = defineStore('report-editor', {
             item.aggs_config.group_by_field_key = [...dimension_list.map(it => it.field_key)]
           } else {
             item.aggs_config = {
-              aggs_type: 'sum',
+              aggs_type: item.metric_config.aggregation_type || 'sum',
               aggs_field_key: item.option.field_key,
               group_by_field_key: [],
             }
@@ -424,6 +430,9 @@ export const useReportEditor = defineStore('report-editor', {
     QueryCommonConfig: (state) => {
       return state.common_config
     },
-
+    // 获取当前度量的聚合方式
+    QueryTargetAggregationType: (state) => {
+      return state.metrics_list.find(item => item.index === state.target_metric_id)?.metric_config?.aggregation_type || 'sum'
+    },
   },
 });

+ 19 - 0
src/modules/conversation-chat/DialogInput.vue

@@ -69,6 +69,7 @@
         @recommend="toRecommend"
         @open_template_select="openTemplateSelect"
         @open_draft="openDraft"
+        @open_data_source="openDataSource"
       />
       <slot name="bottom"></slot>
     </div>
@@ -88,6 +89,13 @@
       @open_draft_manage="openDraftManage"
       @edit-draft="handleEditTemplate"
     ></DraftModal>
+    <DataSourceModal
+      v-if="show_data_source_model"
+      :show_modal="show_data_source_model"
+      @to_back="closeDataSourceModal"
+      @open_data_source_manage="openDataSourceManage"
+      @edit-data-source="handleEditDataSource"
+    ></DataSourceModal>
   </div>
 </template>
 
@@ -101,6 +109,7 @@ import TextAreaTemplateSelected from './dialog-input/TextAreaTemplateSelected.vu
 
 import TemplateSelectModal from '../template-manage/TempalteSelectModal.vue';
 import DraftModal from '../draft-manage/DraftModal.vue';
+import DataSourceModal from '../data-source-manage/DataSourceModal.vue';
 // import FileUploadPreviewV1 from '../file-preview/FileUploadPreviewV1.vue';
 // import useFileSelect from './use-file-select';
 // import useFileSelectedV2 from './use-file-select-v2';
@@ -166,6 +175,8 @@ const show_template_input = ref(false);
 const show_template_select_model = ref(false);
 // 是否展示草稿弹窗
 const show_draft_model = ref(false);
+// 是否展示数据源弹窗
+const show_data_source_model = ref(false);
 // 选中的模板
 const selected_template = ref(null);
 // 是否显示上传模板按钮
@@ -417,6 +428,14 @@ const openDraft = () => {
 const closeDraftModal = () => {
   show_draft_model.value = false;
 };
+// 打开数据源弹窗
+const openDataSource = () => {
+  show_data_source_model.value = true;
+};
+// 关闭数据源弹窗
+const closeDataSourceModal = () => {
+  show_data_source_model.value = false;
+};
 // 关闭模板选择弹窗
 const closeTemplateSelectModal = () => {
   show_template_select_model.value = false;

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

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

+ 10 - 1
src/modules/conversation-chat/dialog-input/InputActions.vue

@@ -10,6 +10,7 @@
       <SettingActions v-if="has_settings" :btn_auth="btn_auth"></SettingActions>
       <RecommendActions v-if="has_recommend" @click="handleRecommend"></RecommendActions>
       <DraftConfig v-if="has_draft" @open_draft="openDraft"></DraftConfig>
+      <DataSourceManage v-if="has_data_source" @open_data_source="openDataSource"></DataSourceManage>
     </div>
 
     <div class="dialog-input-action-right">
@@ -59,6 +60,7 @@ import RecommendActions from './RecommendActions.vue';
 import RecommendModal from './RecommendModal.vue';
 import TemplateConfig from './TemplateConfig.vue';
 import DraftConfig from './DraftConfig.vue';
+import DataSourceManage from './DataSourceManage.vue';
 import { Message } from '@arco-design/web-vue';
 import submit_normal_a from '../../../assets/chat-input/submit_normal.png';
 import submit_normal_b from '../../../assets/chat-input/submit_normal_b.png';
@@ -179,7 +181,8 @@ const emit = defineEmits([
   'upload_file_For_Outline',
   'changeRecommendStatus',
   'open_template_select',
-  'open_draft'
+  'open_draft',
+  'open_data_source'
 ]);
 
 const btn_auth = ref();
@@ -258,6 +261,9 @@ const has_template = computed(() => {
 const has_draft = computed(() => {
   return props.chat_type === 'bgsc';
 });
+const has_data_source = computed(() => {
+  return props.chat_type === 'bgsc';
+});
 
 /**
  * 按钮状态
@@ -291,6 +297,9 @@ const openTemplateSelect = () => {
 const openDraft = () => {
   emit('open_draft');
 };
+const openDataSource = () => {
+  emit('open_data_source');
+};
 
 const handleSubmit = () => {
   emit('submit');

+ 88 - 0
src/modules/conversation-dialog/api-template.js

@@ -253,3 +253,91 @@ export const getRegionEnum = async ({ enum_type_id }) => {
       });
   })
 }
+
+// 获取数据源列表
+export const getDataSourceList = ({ metric_name, page, size }) => {
+  return new Promise((resolve, reject) => {
+    ajax.post(
+      '/iaserverapi/v1/report/flexibleTemplate/manager/metric/list',
+      {
+        metric_name,
+        page,
+        size
+      },
+      {
+        headers: {
+          'Content-Type': 'application/json'
+        }
+      }
+    ).then((res) => {
+      resolve({ code: res.code, data: res.data, pages: { total: res.total } });
+    })
+      .catch((err) => {
+        reject(err);
+      });
+  })
+}
+// 配置指标,新增/更新
+export const createOrUpdateMetric = ({ metric_id, config }) => {
+  return new Promise((resolve, reject) => {
+    ajax.post(
+      '/iaserverapi/v1/report/flexibleTemplate/manager/metric/createOrUpdate',
+      {
+        metric_id,
+        config
+      },
+      {
+        headers: {
+          'Content-Type': 'application/json'
+        }
+      }
+    ).then((res) => {
+      resolve({ code: res.code, data: res.data });
+    })
+      .catch((err) => {
+        reject(err);
+      });
+  })
+}
+// 获取数据源详情
+export const getDataSourceItemDetail = ({ metric_id }) => {
+  return new Promise((resolve, reject) => {
+    ajax.post(
+      '/iaserverapi/v1/report/flexibleTemplate/manager/metric/config/info',
+      {
+        metric_id
+      },
+      {
+        headers: {
+          'Content-Type': 'application/json'
+        }
+      }
+    ).then((res) => {
+      resolve({ code: res.code, data: res.data });
+    })
+      .catch((err) => {
+        reject(err);
+      });
+  })
+}
+// 删除数据源
+export const deleteDataSourceItem = ({ metric_id }) => {
+  return new Promise((resolve, reject) => {
+    ajax.post(
+      '/iaserverapi/v1/report/flexibleTemplate/manager/metric/config/del',
+      {
+        metric_id
+      },
+      {
+        headers: {
+          'Content-Type': 'application/json'
+        }
+      }
+    ).then((res) => {
+      resolve({ code: res.code, data: res.data });
+    })
+      .catch((err) => {
+        reject(err);
+      });
+  })
+}

+ 115 - 0
src/modules/data-source-manage/DataSourceModal.vue

@@ -0,0 +1,115 @@
+<template>
+  <a-modal v-model:visible="visible" width="100%" modal-class="my_custom_template" :mask="false">
+    <div class="container">
+      <MangeHeader @to_back="toBack"></MangeHeader>
+      <!-- 模板管理 -->
+      <div class="container-body">
+        <DataSourceHeader
+          @toSearch="handleSearch"
+          @deleteSelected="deleteSelected"
+          @addDataSource="addDataSource"
+        ></DataSourceHeader>
+        <DataSourceBody ref="draftBodyRef" @edit-datasource="editDatasource"></DataSourceBody>
+      </div>
+    </div>
+    <AddSourceModel
+      v-if="show_add_source_data_model"
+      :type="data_source_type"
+      :metric_id="target_metric_id"
+      @to_back_source="toBackSource"
+      @to_back_save="toBackSave"
+    ></AddSourceModel>
+  </a-modal>
+</template>
+
+<script setup lang="js">
+import { ref,computed,onMounted } from 'vue'
+import MangeHeader from './MangeHeader.vue'
+import DataSourceHeader from './data-source/DataSourceHeader.vue'
+import DataSourceBody from './data-source/DataSourceBody.vue'
+import AddSourceModel from './add-source/AddSourceModel.vue'
+
+const draftBodyRef = ref(null);
+const emit = defineEmits(['to_back', 'edit-datasource']);
+
+const show_add_source_data_model = ref(false);
+const target_metric_id = ref('')
+const data_source_type =ref('add')
+
+
+const visible = computed(()=>{
+  return true
+})
+
+
+const toBack = () => {
+  emit('to_back');
+}
+const toBackSource = () => {
+  show_add_source_data_model.value = false;
+}
+const toBackSave = () => {
+  show_add_source_data_model.value = false;
+  target_metric_id.value = ''
+  draftBodyRef.value.toSearch({type:'metric_id',value:''});
+}
+
+const handleSearch = async (params) => {
+draftBodyRef.value.toSearch(params);
+}
+
+const deleteSelected = async () => {
+  draftBodyRef.value.deleteSelected();
+}
+const editDatasource = (datasourceItem) => {
+  console.log('editDatasource', datasourceItem);
+  target_metric_id.value = datasourceItem.id;
+  show_add_source_data_model.value = true;
+  data_source_type.value = 'edit';
+
+  // emit('edit-datasource', draft);
+}
+
+const addDataSource = () => {
+  show_add_source_data_model.value = true;
+  data_source_type.value = 'add';
+  target_metric_id.value = ''
+}
+
+onMounted(async () => {
+  // await getTemplateList();
+})
+
+</script>
+<style scoped lang="css">
+.template-container {
+  width: 100%;
+  height: 100%;
+}
+.container {
+  height: 100%;
+  box-sizing: border-box;
+  padding: 12px 10px;
+  font-family: '思源黑体';
+}
+.container-body {
+  height: calc(100% - 26px);
+  box-sizing: border-box;
+}
+</style>
+<style>
+.my_custom_template {
+  height: 100vh;
+}
+.my_custom_template .arco-modal-header {
+  display: none;
+}
+.my_custom_template .arco-modal-footer {
+  display: none;
+}
+.my_custom_template .arco-modal-body {
+  height: 100%;
+  box-sizing: border-box;
+  padding: 0px;
+}
+</style>

+ 34 - 0
src/modules/data-source-manage/MangeHeader.vue

@@ -0,0 +1,34 @@
+<template>
+  <div class="template-header">
+    <div class="header" @click="toBack">
+      <icon-left />
+      <div>{{ header_title }}</div>
+    </div>
+  </div>
+</template>
+
+<script setup >
+import { ref } from 'vue';
+
+const emit = defineEmits(['to_back']);
+
+const header_title = ref('数据源管理');
+
+const toBack = () => {
+  emit('to_back');
+};
+</script>
+<style scoped lang="css">
+.template-header {
+  height: 26px;
+}
+.header {
+  width: fit-content;
+  display: flex;
+  align-items: center;
+  gap: 5px;
+  font-weight: 600;
+  font-size: 16px;
+  cursor: pointer;
+}
+</style>

+ 43 - 0
src/modules/data-source-manage/add-source/AddSourceHeader.vue

@@ -0,0 +1,43 @@
+<template>
+  <div class="template-header">
+    <div class="header" @click="toBack">
+      <icon-left />
+      <div>{{ header_title }}</div>
+    </div>
+  </div>
+</template>
+
+<script setup >
+import { ref, computed } from 'vue';
+
+const props = defineProps({
+  type: {
+    type: String,
+    default: 'add'
+  }
+});
+
+const emit = defineEmits(['to_back']);
+
+const header_title = computed(() => {
+  return props.type === 'add' ? '添加数据源' : '修改数据源';
+});
+
+const toBack = () => {
+  emit('to_back');
+};
+</script>
+<style scoped lang="css">
+.template-header {
+  height: 26px;
+}
+.header {
+  width: fit-content;
+  display: flex;
+  align-items: center;
+  gap: 5px;
+  font-weight: 600;
+  font-size: 16px;
+  cursor: pointer;
+}
+</style>

+ 309 - 0
src/modules/data-source-manage/add-source/AddSourceModel.vue

@@ -0,0 +1,309 @@
+<template>
+  <a-modal v-model:visible="visible" width="100%" modal-class="my_custom_template" :mask="false">
+    <img class="bg-aside" src="../../../assets/bg/bg-aside.svg" alt="" />
+    <img class="bg-left-footer" src="../../../assets/bg/bg-left-footer.svg" alt="" />
+    <img class="bg-right-footer" src="../../../assets/bg/bg-right-footer.svg" alt="" />
+    <div class="container">
+      <AddSourceHeader :type="type" @to_back="toBack"></AddSourceHeader>
+      <div class="container-body">
+        <BasicInfo v-model="formData.basicInfo" @type-change="handleTypeChange" />
+
+        <!-- 根据数据源类型显示不同的API连接配置组件 -->
+        <ApiConfig v-if="dataSourceType === '1'" v-model="formData.apiConfig" />
+        <DbConfig v-else-if="dataSourceType === '2'" v-model="formData.dbConfig" />
+
+        <!-- 根据数据源类型显示不同的字段配置组件 -->
+        <ApiFieldConfig v-if="dataSourceType === '1'" v-model="formData.apiFieldConfig" />
+        <DbFieldConfig v-else-if="dataSourceType === '2'" v-model="formData.dbFieldConfig" />
+
+        <div class="save-btn-container">
+          <a-button type="primary" @click="handleSaveAll">全部保存</a-button>
+        </div>
+      </div>
+    </div>
+  </a-modal>
+</template>
+
+<script setup lang="js">
+import { ref, computed, onMounted ,watch} from 'vue'
+import AddSourceHeader from './AddSourceHeader.vue'
+import BasicInfo from './components/BasicInfo.vue'
+import ApiConfig from './components/ApiConfig.vue'
+import DbConfig from './components/DbConfig.vue'
+import ApiFieldConfig from './components/ApiFieldConfig.vue'
+import DbFieldConfig from './components/DbFieldConfig.vue'
+import { Message } from '@arco-design/web-vue';
+import { createOrUpdateMetric,getDataSourceItemDetail} from '../../conversation-dialog/api-template.js'
+
+const props = defineProps({
+  type: {
+    type: String,
+    default: 'add'
+  },
+  data: {
+    type: Object,
+    default: () => {}
+  },
+  metric_id: {
+    type: String,
+    default: ''
+  },
+});
+
+const emit = defineEmits(['to_back_source', 'edit-draft','to_back_save']);
+
+const visible = computed(() => {
+  return true
+});
+
+// 数据源类型
+const dataSourceType = ref('1');
+
+// 表单数据
+const formData = ref({
+  basicInfo: {
+    metric_name: '',
+    data_source_type: '1',
+    api_description: ''
+  },
+  apiConfig: {
+    api_url: '',
+    api_method: 'GET',
+    api_params: [],
+    data_level: '',
+    data_result_type: 'list'
+  },
+  dbConfig: {
+    db_sql_template: '',
+    data_level: '',
+    data_result_type: 'list'
+  },
+  apiFieldConfig: {
+    metric_fields: []
+  },
+  dbFieldConfig: {
+    metric_fields: []
+  }
+});
+
+// 处理数据源类型变化
+const handleTypeChange = (type) => {
+  dataSourceType.value = type;
+  formData.value.basicInfo.data_source_type = type;
+};
+
+// 全部保存
+const handleSaveAll = async () => {
+  // 整合所有表单数据为API参数格式
+  const allData = {
+    metric_config: {
+      ...formData.value.basicInfo,
+      ...(dataSourceType.value === '1' ? formData.value.apiConfig : formData.value.dbConfig)
+    },
+    metric_fields_config: {
+      metric_fields: (dataSourceType.value === '1' ? formData.value.apiFieldConfig : formData.value.dbFieldConfig).metric_fields
+    }
+  };
+
+  try {
+    // const config = JSON.stringify(allData);
+    const data = {
+      config: allData
+    }
+    if(props.metric_id) {
+      data.metric_id = props.metric_id;
+    }
+    // 发送保存请求
+    const response = await createOrUpdateMetricFunc(data);
+    console.log('保存成功:', response);
+    // 保存成功后,跳转到列表页
+    if(props.type === 'add') {
+        Message.success('新增成功');
+        emit('to_back_save');
+    }else {
+        Message.success('更新成功');
+    }
+
+  } catch (error) {
+    console.error('保存失败:', error);
+
+  }
+};
+
+const createOrUpdateMetricFunc = async (data) => {
+  const res = await createOrUpdateMetric(data);
+  return res;
+};
+
+const toBack = () => {
+  emit('to_back_source');
+};
+
+const editDraft = (draft) => {
+  emit('edit-draft', draft);
+};
+
+const getDataSourceItemDetailFunc = async (data) => {
+  try {
+    const res = await getDataSourceItemDetail(data);
+    if(res.code/1 === 200){
+      const allData = JSON.parse(res.data.configs);
+      console.log('解析后的数据:', allData);
+
+      // 回填基础信息
+      formData.value.basicInfo = {
+        metric_name: allData.metric_config?.metric_name || '',
+        data_source_type: allData.metric_config?.data_source_type || '1',
+        api_description: allData.metric_config?.api_description || ''
+      };
+
+      // 确定数据源类型
+      const sourceType = allData.metric_config?.data_source_type || '1';
+      dataSourceType.value = sourceType;
+
+      // 回填配置信息
+      if (sourceType === '1') {
+        // API类型
+        formData.value.apiConfig = {
+          api_url: allData.metric_config?.api_url || '',
+          api_method: allData.metric_config?.api_method || 'GET',
+          api_params: allData.metric_config?.api_params || [],
+          data_level: allData.metric_config?.data_level || '',
+          data_result_type: allData.metric_config?.data_result_type || 'list'
+        };
+
+        // 回填API字段配置
+        formData.value.apiFieldConfig = {
+          metric_fields: allData.metric_fields_config?.metric_fields || []
+        };
+      } else if (sourceType === '2') {
+        // 数据库类型
+        formData.value.dbConfig = {
+          db_sql_template: allData.metric_config?.db_sql_template || '',
+          data_level: allData.metric_config?.data_level || '',
+          data_result_type: allData.metric_config?.data_result_type || 'list'
+        };
+
+        // 回填数据库字段配置
+        formData.value.dbFieldConfig = {
+          metric_fields: allData.metric_fields_config?.metric_fields || []
+        };
+      }
+
+      console.log('数据回填完成:', formData.value);
+    }
+
+  }catch(error){
+    console.error('获取数据源详情失败:', error);
+  }
+};
+
+onMounted(async () => {
+  // 初始化数据
+  // if (props.data) {
+  //   formData.value = {
+  //     basicInfo: props.data.metric_config || {
+  //       metric_name: '',
+  //       data_source_type: '1',
+  //       api_description: ''
+  //     },
+  //     apiConfig: props.data.metric_config || {
+  //       api_url: '',
+  //       api_method: 'GET',
+  //       api_params: [],
+  //       data_level: '',
+  //       data_result_type: 'list'
+  //     },
+  //     dbConfig: props.data.metric_config || {
+  //       db_sql_template: '',
+  //       data_level: '',
+  //       data_result_type: 'list'
+  //     },
+  //     apiFieldConfig: props.data.metric_fields_config || {
+  //       metric_fields: []
+  //     },
+  //     dbFieldConfig: props.data.metric_fields_config || {
+  //       metric_fields: []
+  //     }
+  //   };
+
+  //   // 设置数据源类型
+  //   dataSourceType.value = formData.value.basicInfo.data_source_type || '1';
+  // }
+});
+watch(()=>props.metric_id, (new_val)=>{
+  if(new_val){
+    getDataSourceItemDetailFunc({metric_id: new_val});
+  }
+},{ immediate: true })
+
+</script>
+<style scoped lang="css">
+.template-container {
+  width: 100%;
+  height: 100%;
+}
+.container {
+  position: relative;
+  height: 100%;
+  box-sizing: border-box;
+  padding: 12px 10px;
+  font-family: '思源黑体';
+  z-index: 10;
+}
+.container-body {
+  height: calc(100% - 26px);
+  box-sizing: border-box;
+  padding: 20px;
+  overflow-y: auto;
+}
+.save-btn-container {
+  position: absolute;
+  top: 50px;
+  right: 50px;
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 30px;
+}
+.bg-aside {
+  position: absolute;
+  left: 0;
+  top: 0;
+  height: 100%;
+  z-index: 0;
+}
+
+.bg-left-footer {
+  position: absolute;
+  left: -18px;
+  bottom: 0;
+  width: 349px;
+  height: 337px;
+  z-index: 0;
+}
+
+.bg-right-footer {
+  position: absolute;
+  right: 0;
+  bottom: 0;
+  width: 389px;
+  height: 597px;
+  z-index: 0;
+}
+</style>
+<style>
+.my_custom_template {
+  height: 100vh;
+}
+.my_custom_template .arco-modal-header {
+  display: none;
+}
+.my_custom_template .arco-modal-footer {
+  display: none;
+}
+.my_custom_template .arco-modal-body {
+  height: 100%;
+  box-sizing: border-box;
+  padding: 0px;
+}
+</style>

+ 183 - 0
src/modules/data-source-manage/add-source/components/ApiConfig.vue

@@ -0,0 +1,183 @@
+<template>
+  <div class="api-config-container">
+    <h3 class="section-title">API连接配置</h3>
+    <div class="form-row">
+      <div class="form-item">
+        <a-form-item field="api_url" label="API URL">
+          <a-input v-model="api_url" @input="handleApiUrlChange" placeholder="/zbgkht_tz/zbgk/queryGwtbzbDataThree" />
+        </a-form-item>
+      </div>
+      <div class="form-item">
+        <a-form-item field="api_method" label="请求方法">
+          <a-select v-model="api_method" @change="handleApiMethodChange">
+            <a-option value="GET">GET</a-option>
+            <a-option value="POST">POST</a-option>
+            <a-option value="PUT">PUT</a-option>
+            <a-option value="DELETE">DELETE</a-option>
+          </a-select>
+        </a-form-item>
+      </div>
+    </div>
+    <div class="form-row">
+      <div class="form-item full-width">
+        <a-form-item field="api_params" label="请求参数">
+          <div class="params-container">
+            <div v-for="(param, index) in api_params" :key="index" class="param-row">
+              <a-input
+                v-model="param.key"
+                @input="handleParamChange"
+                placeholder="参数名"
+                style="flex: 1; margin-right: 10px"
+              />
+              <a-input
+                v-model="param.value"
+                @input="handleParamChange"
+                placeholder="参数值"
+                style="flex: 1; margin-right: 10px"
+              />
+              <a-button type="outline" status="danger" @click="removeParam(param.id)">
+                <icon-delete />
+              </a-button>
+            </div>
+            <a-button type="outline" @click="addParam">+ 添加参数</a-button>
+          </div>
+        </a-form-item>
+      </div>
+    </div>
+    <div class="form-row">
+      <div class="form-item">
+        <a-form-item field="data_level" label="数据级别">
+          <a-input v-model="data_level" @input="handleDataLevelChange" placeholder="数据级别" />
+        </a-form-item>
+      </div>
+      <div class="form-item">
+        <a-form-item field="data_result_type" label="返回数据类型">
+          <a-select v-model="data_result_type" @change="handleDataResultTypeChange">
+            <a-option value="list">列表</a-option>
+            <a-option value="object">对象</a-option>
+          </a-select>
+        </a-form-item>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="js">
+import { ref, watch } from 'vue';
+
+const props = defineProps({
+  modelValue: {
+    type: Object,
+    default: () => ({})
+  }
+});
+
+const emit = defineEmits(['update:modelValue']);
+
+// 使用独立的响应式变量
+const api_url = ref(props.modelValue.api_url || '');
+const api_method = ref(props.modelValue.api_method || 'GET');
+const api_params = ref(props.modelValue.api_params || []);
+const data_level = ref(props.modelValue.data_level || '');
+const data_result_type = ref(props.modelValue.data_result_type || 'list');
+
+// 监听props变化,更新本地数据
+watch(() => props.modelValue, (newValue) => {
+  api_url.value = newValue.api_url || '';
+  api_method.value = newValue.api_method || 'GET';
+  api_params.value = newValue.api_params || [];
+  data_level.value = newValue.data_level || '';
+  data_result_type.value = newValue.data_result_type || 'list';
+}, { deep: true });
+
+// 通知父组件数据变化
+const notifyParent = () => {
+  emit('update:modelValue', {
+    api_url: api_url.value,
+    api_method: api_method.value,
+    api_params: api_params.value,
+    data_level: data_level.value,
+    data_result_type: data_result_type.value
+  });
+};
+
+// 处理各个字段的变化
+const handleApiUrlChange = () => {
+  notifyParent();
+};
+
+const handleApiMethodChange = () => {
+  notifyParent();
+};
+
+const handleDataLevelChange = () => {
+  notifyParent();
+};
+
+const handleDataResultTypeChange = () => {
+  notifyParent();
+};
+
+const handleParamChange = () => {
+  notifyParent();
+};
+
+// 添加参数
+const addParam = () => {
+  api_params.value = [...api_params.value, { id: `${Date.now()+Math.floor(Math.random() * 100)}`, key: '', value: '' }];
+  notifyParent();
+};
+
+// 删除参数
+const removeParam = (id) => {
+  console.log(id,999)
+  api_params.value = api_params.value.filter((param) => param.id !== id);
+  console.log(api_params.value,888)
+  // api_params.value = api_params.value.filter((param) => param.id !== id);
+  notifyParent();
+};
+</script>
+
+<style scoped lang="css">
+.api-config-container {
+  width: 60%;
+  background-color: #ffffff;
+  padding: 20px;
+  border-radius: 8px;
+  margin-bottom: 20px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.section-title {
+  font-size: 16px;
+  font-weight: 600;
+  margin-bottom: 20px;
+  color: #333;
+}
+
+.form-row {
+  display: flex;
+  gap: 20px;
+  margin-bottom: 16px;
+}
+
+.form-item {
+  flex: 1;
+}
+
+.form-item.full-width {
+  flex: 1 1 100%;
+}
+
+.params-container {
+  border: 1px solid #d9d9d9;
+  border-radius: 4px;
+  padding: 10px;
+}
+
+.param-row {
+  display: flex;
+  align-items: center;
+  margin-bottom: 10px;
+}
+</style>

+ 213 - 0
src/modules/data-source-manage/add-source/components/ApiFieldConfig.vue

@@ -0,0 +1,213 @@
+<template>
+  <div class="api-field-config-container">
+    <div class="section-header">
+      <h3 class="section-title">字段配置</h3>
+      <div class="add-field-btn-container">
+        <a-button type="primary" @click="addField">+ 添加字段</a-button>
+      </div>
+    </div>
+    <div class="table-wrapper">
+      <table class="field-table">
+        <thead>
+          <tr>
+            <th>字段名称</th>
+            <th>字段描述</th>
+            <th>字段类型</th>
+            <th>字段Key</th>
+            <th>字段角色</th>
+            <th>是否可聚合</th>
+            <th>是否必填</th>
+            <th>通用配置类型</th>
+            <th>是否需要展示字段</th>
+            <th>接口参数是否支持多值</th>
+            <th>接口参数是否支持模糊查询</th>
+            <th>多值分隔符</th>
+            <th>枚举类型ID</th>
+            <th>操作</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-for="(field, index) in metric_fields" :key="index">
+            <td><a-input v-model="field.field_name" @input="handleFieldChange" placeholder="字段名称" /></td>
+            <td>
+              <a-input v-model="field.field_description" @input="handleFieldChange" placeholder="字段描述" />
+            </td>
+            <td>
+              <a-select v-model="field.field_type" @change="handleFieldChange">
+                <a-option value="int">int</a-option>
+                <a-option value="date">date</a-option>
+                <a-option value="str">str</a-option>
+              </a-select>
+            </td>
+            <td><a-input v-model="field.field_key" @input="handleFieldChange" placeholder="字段Key" /></td>
+            <td>
+              <a-select v-model="field.column_role" @change="handleFieldChange">
+                <a-option value="dim">维度</a-option>
+                <a-option value="meas">度量</a-option>
+              </a-select>
+            </td>
+            <td>
+              <a-select v-model="field.is_aggregatable" @change="handleFieldChange">
+                <a-option :value="0">否</a-option>
+                <a-option :value="1">是</a-option>
+              </a-select>
+            </td>
+            <td>
+              <a-select v-model="field.is_required" @change="handleFieldChange">
+                <a-option :value="0">否</a-option>
+                <a-option :value="1">是</a-option>
+              </a-select>
+            </td>
+            <td>
+              <a-input v-model="field.common_config_type" @input="handleFieldChange" placeholder="通用配置类型" />
+            </td>
+            <td>
+              <a-select v-model="field.is_needs_show_fields" @change="handleFieldChange">
+                <a-option :value="0">否</a-option>
+                <a-option :value="1">是</a-option>
+              </a-select>
+            </td>
+            <td>
+              <a-select v-model="field.is_api_support_multi_value" @change="handleFieldChange">
+                <a-option :value="0">否</a-option>
+                <a-option :value="1">是</a-option>
+              </a-select>
+            </td>
+            <td>
+              <a-select v-model="field.is_api_support_fuzzy_query" @change="handleFieldChange">
+                <a-option :value="0">否</a-option>
+                <a-option :value="1">是</a-option>
+              </a-select>
+            </td>
+            <td>
+              <a-input v-model="field.multi_value_delimiter" @input="handleFieldChange" placeholder="多值分隔符" />
+            </td>
+            <td>
+              <a-select v-model="field.enum_type_id" @change="handleFieldChange">
+                <a-option :value="0">无</a-option>
+                <a-option :value="1">有</a-option>
+              </a-select>
+            </td>
+            <td>
+              <a-button type="outline" status="danger" @click="deleteField(index)"><icon-delete /></a-button>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+  </div>
+</template>
+
+<script setup lang="js">
+import { ref, watch } from 'vue';
+
+const props = defineProps({
+  modelValue: {
+    type: Object,
+    default: () => ({})
+  }
+});
+
+const emit = defineEmits(['update:modelValue']);
+
+// 使用独立的响应式变量
+const metric_fields = ref(props.modelValue.metric_fields || []);
+
+// 监听props变化,更新本地数据
+watch(() => props.modelValue, (newValue) => {
+  metric_fields.value = newValue.metric_fields || [];
+}, { deep: true });
+
+// 通知父组件数据变化
+const notifyParent = () => {
+  emit('update:modelValue', {
+    metric_fields: metric_fields.value
+  });
+};
+
+// 处理字段变化
+const handleFieldChange = () => {
+  notifyParent();
+};
+
+// 添加字段
+const addField = () => {
+  metric_fields.value = [...metric_fields.value, {
+    field_name: '',
+    field_description: '',
+    field_type: 'str',
+    field_key: '',
+    column_role: 'dim',
+    is_aggregatable: 0,
+    is_required: 0,
+    common_config_type: null,
+    is_api_support_multi_value: 0,
+    is_api_support_fuzzy_query: 0,
+    multi_value_delimiter: ',',
+    is_needs_show_fields: 1,
+    enum_type_id: 0
+  }];
+  notifyParent();
+};
+
+// 删除字段
+const deleteField = (index) => {
+  metric_fields.value = metric_fields.value.filter((_, i) => i !== index);
+  notifyParent();
+};
+</script>
+
+<style scoped lang="css">
+.api-field-config-container {
+  /* width: 90%; */
+  background-color: #ffffff;
+  padding: 20px;
+  border-radius: 8px;
+  margin-bottom: 20px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+.section-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.section-title {
+  font-size: 16px;
+  font-weight: 600;
+  margin-bottom: 20px;
+  color: #333;
+}
+
+.table-wrapper {
+  overflow-x: auto;
+  margin-bottom: 16px;
+}
+
+.field-table {
+  width: 100%;
+  border-collapse: collapse;
+  min-width: 1200px;
+}
+
+.field-table th,
+.field-table td {
+  padding: 8px;
+  border: 1px solid #e8e8e8;
+  text-align: left;
+}
+
+.field-table th {
+  background-color: #f5f5f5;
+  font-weight: 600;
+  white-space: nowrap;
+}
+
+.field-table td {
+  white-space: nowrap;
+}
+
+.add-field-btn-container {
+  margin-top: 16px;
+}
+</style>

+ 127 - 0
src/modules/data-source-manage/add-source/components/BasicInfo.vue

@@ -0,0 +1,127 @@
+<template>
+  <div class="basic-info-container">
+    <h3 class="section-title">基础信息</h3>
+    <div class="form-row">
+      <div class="form-item">
+        <a-form-item field="metric_name" label="数据源名称">
+          <a-input v-model="metric_name" @input="handleNameChange" placeholder="请输入数据源名称" />
+        </a-form-item>
+      </div>
+      <div class="form-item">
+        <a-form-item field="data_source_type" label="数据源类型">
+          <a-select v-model="selectedType" @change="handleTypeChange">
+            <a-option value="1">API接口</a-option>
+            <a-option value="2">数据库类型</a-option>
+          </a-select>
+        </a-form-item>
+      </div>
+    </div>
+    <div class="form-row">
+      <div class="form-item full-width">
+        <a-form-item field="api_description" label="描述">
+          <a-textarea
+            v-model="api_description"
+            @blur="handleDescriptionChange"
+            placeholder="请输入数据源描述信息"
+            :rows="3"
+          />
+        </a-form-item>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="js">
+import { ref, watch } from 'vue';
+
+const props = defineProps({
+  modelValue: {
+    type: Object,
+    default: () => ({})
+  }
+});
+
+const emit = defineEmits(['update:modelValue', 'type-change']);
+
+// 使用独立的响应式变量
+const metric_name = ref(props.modelValue.metric_name || '');
+const data_source_type = ref(props.modelValue.data_source_type || '1');
+const api_description = ref(props.modelValue.api_description || '');
+
+// 使用独立的变量来绑定选择器
+const selectedType = ref('1'); // 强制设置默认值为字符串类型'1'
+
+// 确保两个变量同步
+selectedType.value = data_source_type.value;
+
+// 监听props变化,更新本地数据
+watch(() => props.modelValue, (newValue) => {
+  metric_name.value = newValue.metric_name || '';
+  data_source_type.value = newValue.data_source_type || '1';
+  api_description.value = newValue.api_description || '';
+  // 确保选择器值同步
+  selectedType.value = data_source_type.value;
+}, { deep: true });
+
+// 通知父组件数据变化
+const notifyParent = () => {
+  emit('update:modelValue', {
+    metric_name: metric_name.value,
+    data_source_type: data_source_type.value,
+    api_description: api_description.value
+  });
+};
+
+// 处理名称变化
+const handleNameChange = () => {
+  notifyParent();
+};
+
+// 处理类型变化
+const handleTypeChange = (value) => {
+  // 更新两个变量
+  selectedType.value = value;
+  data_source_type.value = value;
+
+  // 通知父组件
+  notifyParent();
+  emit('type-change', value);
+};
+
+// 处理描述变化
+const handleDescriptionChange = () => {
+  notifyParent();
+};
+</script>
+
+<style scoped lang="css">
+.basic-info-container {
+  width: 60%;
+  background-color: #ffffff;
+  padding: 20px;
+  border-radius: 8px;
+  margin-bottom: 20px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.section-title {
+  font-size: 16px;
+  font-weight: 600;
+  margin-bottom: 20px;
+  color: #333;
+}
+
+.form-row {
+  display: flex;
+  gap: 20px;
+  margin-bottom: 16px;
+}
+
+.form-item {
+  flex: 1;
+}
+
+.form-item.full-width {
+  flex: 1 1 100%;
+}
+</style>

+ 111 - 0
src/modules/data-source-manage/add-source/components/DbConfig.vue

@@ -0,0 +1,111 @@
+<template>
+  <div class="db-config-container">
+    <h3 class="section-title">API连接配置</h3>
+    <div class="form-row">
+      <div class="form-item full-width">
+        <a-form-item field="db_sql_template" label="SQL模板">
+          <a-textarea
+            v-model="db_sql_template"
+            @blur="handleSqlTemplateChange"
+            placeholder="select {select_fields} from TEST.SALE_INFO where 1=1 {where_clause} {group_by_fields} {order_by_fields} {limit_fields};"
+            :rows="3"
+          />
+        </a-form-item>
+      </div>
+    </div>
+    <div class="form-row">
+      <div class="form-item">
+        <a-form-item field="data_level" label="数据级别">
+          <a-input v-model="data_level" @input="handleDataLevelChange" placeholder="数据级别" />
+        </a-form-item>
+      </div>
+      <div class="form-item">
+        <a-form-item field="data_result_type" label="返回数据类型">
+          <a-select v-model="data_result_type" @change="handleDataResultTypeChange">
+            <a-option value="list">列表</a-option>
+            <a-option value="object">对象</a-option>
+          </a-select>
+        </a-form-item>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="js">
+import { ref, watch } from 'vue';
+
+const props = defineProps({
+  modelValue: {
+    type: Object,
+    default: () => ({})
+  }
+});
+
+const emit = defineEmits(['update:modelValue']);
+
+// 使用独立的响应式变量
+const db_sql_template = ref(props.modelValue.db_sql_template || '');
+const data_level = ref(props.modelValue.data_level || '');
+const data_result_type = ref(props.modelValue.data_result_type || 'list');
+
+// 监听props变化,更新本地数据
+watch(() => props.modelValue, (newValue) => {
+  db_sql_template.value = newValue.db_sql_template || '';
+  data_level.value = newValue.data_level || '';
+  data_result_type.value = newValue.data_result_type || 'list';
+}, { deep: true });
+
+// 通知父组件数据变化
+const notifyParent = () => {
+  emit('update:modelValue', {
+    db_sql_template: db_sql_template.value,
+    data_level: data_level.value,
+    data_result_type: data_result_type.value
+  });
+};
+
+// 处理各个字段的变化
+const handleSqlTemplateChange = () => {
+  notifyParent();
+};
+
+const handleDataLevelChange = () => {
+  notifyParent();
+};
+
+const handleDataResultTypeChange = () => {
+  notifyParent();
+};
+</script>
+
+<style scoped lang="css">
+.db-config-container {
+  width: 60%;
+  background-color: #ffffff;
+  padding: 20px;
+  border-radius: 8px;
+  margin-bottom: 20px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.section-title {
+  font-size: 16px;
+  font-weight: 600;
+  margin-bottom: 20px;
+  color: #333;
+}
+
+.form-row {
+  display: flex;
+  gap: 20px;
+  margin-bottom: 16px;
+}
+
+.form-item {
+  flex: 1;
+}
+
+.form-item.full-width {
+  flex: 1 1 100%;
+}
+</style>

+ 194 - 0
src/modules/data-source-manage/add-source/components/DbFieldConfig.vue

@@ -0,0 +1,194 @@
+<template>
+  <div class="db-field-config-container">
+    <div class="section-header">
+      <h3 class="section-title">字段配置</h3>
+      <div class="add-field-btn-container">
+        <a-button type="primary" @click="addField">+ 添加字段</a-button>
+      </div>
+    </div>
+    <div class="table-wrapper">
+      <table class="field-table">
+        <thead>
+          <tr>
+            <th>字段名称</th>
+            <th>字段描述</th>
+            <th>字段类型</th>
+            <th>字段Key</th>
+            <th>字段角色</th>
+            <th>是否可聚合</th>
+            <th>是否必填</th>
+            <th>是否需要展示字段</th>
+            <th>字段来源表</th>
+            <th>枚举类型ID</th>
+            <th>操作</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-for="(field, index) in metric_fields" :key="index">
+            <td><a-input v-model="field.field_name" @input="handleFieldChange" placeholder="字段名称" /></td>
+            <td>
+              <a-input v-model="field.field_description" @input="handleFieldChange" placeholder="字段描述" />
+            </td>
+            <td>
+              <a-select v-model="field.field_type" @change="handleFieldChange">
+                <a-option value="int">int</a-option>
+                <a-option value="date">date</a-option>
+                <a-option value="str">str</a-option>
+              </a-select>
+            </td>
+            <td><a-input v-model="field.field_key" @input="handleFieldChange" placeholder="字段Key" /></td>
+            <td>
+              <a-select v-model="field.column_role" @change="handleFieldChange">
+                <a-option value="dim">维度</a-option>
+                <a-option value="meas">度量</a-option>
+              </a-select>
+            </td>
+            <td>
+              <a-select v-model="field.is_aggregatable" @change="handleFieldChange">
+                <a-option :value="0">否</a-option>
+                <a-option :value="1">是</a-option>
+              </a-select>
+            </td>
+            <td>
+              <a-select v-model="field.is_required" @change="handleFieldChange">
+                <a-option :value="0">否</a-option>
+                <a-option :value="1">是</a-option>
+              </a-select>
+            </td>
+            <td>
+              <a-select v-model="field.is_needs_show_fields" @change="handleFieldChange">
+                <a-option :value="0">否</a-option>
+                <a-option :value="1">是</a-option>
+              </a-select>
+            </td>
+            <td>
+              <a-input v-model="field.field_from_table" @input="handleFieldChange" placeholder="字段来源表" />
+            </td>
+            <td>
+              <a-select v-model="field.enum_type_id" @change="handleFieldChange">
+                <a-option :value="0">无</a-option>
+                <a-option :value="1">有</a-option>
+              </a-select>
+              <!-- <a-input v-model="field.enum_type_id" @input="handleFieldChange" placeholder="枚举类型ID" /> -->
+            </td>
+            <td>
+              <a-button type="outline" status="danger" @click="deleteField(index)"><icon-delete /></a-button>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+  </div>
+</template>
+
+<script setup lang="js">
+import { ref, watch } from 'vue';
+
+const props = defineProps({
+  modelValue: {
+    type: Object,
+    default: () => ({})
+  }
+});
+
+const emit = defineEmits(['update:modelValue']);
+
+// 使用独立的响应式变量
+const metric_fields = ref(props.modelValue.metric_fields || []);
+
+// 监听props变化,更新本地数据
+watch(() => props.modelValue, (newValue) => {
+  metric_fields.value = newValue.metric_fields || [];
+}, { deep: true });
+
+// 通知父组件数据变化
+const notifyParent = () => {
+  emit('update:modelValue', {
+    metric_fields: metric_fields.value
+  });
+};
+
+// 处理字段变化
+const handleFieldChange = () => {
+  notifyParent();
+};
+
+// 添加字段
+const addField = () => {
+  metric_fields.value = [...metric_fields.value, {
+    field_name: '',
+    field_description: '',
+    field_type: 'str',
+    field_key: '',
+    column_role: 'dim',
+    is_aggregatable: 0,
+    is_required: 0,
+    common_config_type: null,
+    field_from_table: '',
+    is_needs_show_fields: 1,
+    enum_type_id: 0
+  }];
+  notifyParent();
+};
+
+// 删除字段
+const deleteField = (index) => {
+  metric_fields.value = metric_fields.value.filter((_, i) => i !== index);
+  notifyParent();
+};
+</script>
+
+<style scoped lang="css">
+.db-field-config-container {
+  /* width: 90%; */
+  background-color: #ffffff;
+  padding: 20px;
+  border-radius: 8px;
+  margin-bottom: 20px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+.section-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.section-title {
+  font-size: 16px;
+  font-weight: 600;
+  margin-bottom: 20px;
+  color: #333;
+}
+
+.table-wrapper {
+  overflow-x: auto;
+  margin-bottom: 16px;
+}
+
+.field-table {
+  width: 100%;
+  border-collapse: collapse;
+  min-width: 1200px;
+}
+
+.field-table th,
+.field-table td {
+  padding: 8px;
+  border: 1px solid #e8e8e8;
+  text-align: left;
+}
+
+.field-table th {
+  background-color: #f5f5f5;
+  font-weight: 600;
+  white-space: nowrap;
+}
+
+.field-table td {
+  white-space: nowrap;
+}
+
+.add-field-btn-container {
+  margin-top: 16px;
+}
+</style>

+ 64 - 0
src/modules/data-source-manage/add-source/参数格式.txt

@@ -0,0 +1,64 @@
+# API类型参数
+        params = {
+            "metric_config": {
+                "metric_name": "管控指标",              # 数据源名称
+                "data_source_type": 1,                 # 数据源类型
+                "api_description": "决策平台接口-管控指标",          # 数据源描述
+                "api_url": "/zbgkht_tz/zbgk/queryGwtbzbDataThree", # 数据源api URL
+                "api_method": "POST",                      # 数据源api 请求方式
+                "api_params": [                         # 数据源接口支持的参数--界面需要配置key、value
+                    {"key": "cityId", "value": "468a81b26aa9a035016aaa3b97560013"},
+                    {"key": "date1", "value": "{date1}"},
+                    {"key": "date2", "value": "{date2}"}
+                ],
+                "data_level": "",                         # 数据源返回数据层级,多层级用"."分隔
+                "data_result_type": "list",             # 数据源返回数据类型,list或dict
+            },
+            "metric_fields_config": {
+                "metric_fields": [
+                    {
+                        "field_name": "当期投运的智能融合终端平均在线率",  # 字段名称
+                        "field_description": "智能融合终端平均在线率",   # 字段描述
+                        "field_type": "int",                             # 字段类型, int/date/str
+                        "field_key": "avg_online_rate",                 # 字段key, 用于数据处理
+                        "column_role": "meas",                           # 字段角色, meas/dim   meas: 度量,dim: 维度
+                        "is_aggregatable": 0,                            # 是否可聚合 1:是 0:否
+                        "is_required": 0,                                 # 是否必填 1:是 0:否
+                        "common_config_type": None,                       # 通用配置类型
+                        "is_api_support_multi_value": 0,                # 接口参数是否支持多值 1:是 0:否
+                        "is_api_support_fuzzy_query": 0,             # 接口参数是否支持模糊查询 1:是 0:否
+                        "multi_value_delimiter": ",",                      # 如果接口参数支持多值,则表示,多值分隔符,默认为逗号
+                        "is_needs_show_fields": 1,                        # 是否需要展示字段 1:是 0:否  配置除目标字段以外的维度字段
+                        "enum_type_id": 0,                            # 枚举类型id 0:无  1:有
+                    }
+                ]
+            }
+        }
+# 数据库类型参数
+        params = {
+            "metric_config": {
+                "metric_name": "品牌指标",              # 数据源名称
+                "data_source_type": 2,                  # 数据源类型
+                "api_description": "测试用品牌指标",          # 数据源描述
+                "db_sql_template": "select {select_fields} from TEST.SALE_INFO where 1=1 {where_clause} {group_by_fields} {order_by_fields} {limit_fields};",                     # 数据源sql模板, eg: select {select_fields} from TEST.SALE_INFO where 1=1 {where_clause} {group_by_fields} {order_by_fields} {limit_fields};
+                "data_level": "",                         # 数据源返回数据层级,多层级用"."分隔
+                "data_result_type": "list"                 # 数据源返回数据类型, list/dict
+            },
+            "metric_fields_config": {
+                "metric_fields": [
+                    {
+                        "field_name": "当期投运的智能融合终端平均在线率(%)",  # 字段名称
+                        "field_description": "智能融合终端平均在线率(%)",   # 字段描述
+                        "field_type": "int",                             # 字段类型, int/date/str
+                        "field_key": "avg_online_rate",                 # 字段key, 用于数据处理
+                        "column_role": "meas",                           # 字段角色, meas/dim   meas: 度量,dim: 维度
+                        "is_aggregatable": 1,                            # 是否可聚合 1:是 0:否
+                        "is_required": 1,                                 # 是否必填 1:是 0:否
+                        "common_config_type": None,                       # 通用配置类型
+                        "field_from_table": "sale_info",                  # 字段来源表
+                        "is_needs_show_fields": 1,                      # 是否需要展示字段 1:是 0:否  配置除目标字段以外的维度字段
+                        "enum_type_id": 0,                            # 枚举类型id 0:无  1:有
+                    }
+                ]
+            }
+        }

+ 402 - 0
src/modules/data-source-manage/data-source/DataSourceBody.vue

@@ -0,0 +1,402 @@
+<template>
+  <div class="detail-body">
+    <a-table
+      :row-selection="rowSelection"
+      v-model:selectedKeys="selectedKeys"
+      :pagination="false"
+      rowKey="id"
+      :data="groupTemplates"
+      :loading="is_Loading"
+      :scroll="{ y: 'calc(100% - 64px)' }"
+      scrollbar
+    >
+      <template #columns>
+        <a-table-column
+          v-for="item in columns"
+          :key="item.dataIndex"
+          :title="item.title"
+          :data-index="item.dataIndex"
+          :header-cell-style="{ backgroundColor: 'var(--table-th-bg)' }"
+          align="left"
+          :width="item.width"
+        >
+          <template #cell="{ record, column }">
+            <div class="column" v-if="item.dataIndex === 'name'">
+              <img src="../../../assets/report-template/template_name.svg" alt="" />
+              {{ record[column.dataIndex] }}
+            </div>
+            <div class="column" v-else-if="item.dataIndex === 'data_source_type'">
+              {{ record[column.dataIndex] / 1 === 1 ? 'API' : '数据库' }}
+            </div>
+            <div class="column" v-else-if="item.dataIndex === 'created_at'">
+              {{ formatDateTime(record[column.dataIndex]) || '-' }}
+            </div>
+            <div class="column" v-else-if="item.dataIndex === 'action'">
+              <span class="link" @click="handleEdit(record)">编辑</span>
+              <span class="link link_del" @click="handleDelete(record)">删除</span>
+            </div>
+            <div class="column" v-else>
+              {{ record[column.dataIndex] }}
+            </div>
+          </template>
+        </a-table-column>
+      </template>
+    </a-table>
+    <div class="pagination">
+      <a-pagination
+        :total="page.total"
+        :current="page.current"
+        :page-size="page.page_size"
+        @change="changePageChange"
+        @page-size-change="changePageSizeChange"
+        show-total
+        show-page-size
+      />
+    </div>
+  </div>
+  <DeleteModal
+    :visible="show_delete_modal"
+    :is_branch="is_branch"
+    @ok="handleConfirm"
+    @cancel="handleCancel"
+  ></DeleteModal>
+</template>
+
+<script setup >
+import { ref, reactive, watch, computed, nextTick, onMounted } from 'vue';
+import { getDataSourceList, deleteDataSourceItem } from '../../conversation-dialog/api-template';
+import DeleteModal from '../../../components/DeleteModal.vue';
+import { Message } from '@arco-design/web-vue';
+import { formatDateTime } from '../../../utils/utils';
+
+const emit = defineEmits(['edit-template']);
+
+const selectedKeys = ref([]);
+const show_delete_modal = ref(false);
+const is_branch = ref(false);
+const selected_id_list = ref([]);
+const filter_config = ref({
+  filter: null,
+  filter_value: null
+});
+
+const rowSelection = reactive({
+  type: 'checkbox',
+  showCheckedAll: true,
+  onlyCurrent: false
+});
+
+const columns = ref([
+  {
+    title: '数据源名称',
+    dataIndex: 'metric_name',
+    key: 'metric_name',
+    width: 200,
+    sortable: {
+      sortDirections: ['ascend', 'descend']
+    }
+  },
+  {
+    title: '描述',
+    dataIndex: 'api_description',
+    key: 'api_description',
+    width: 400
+  },
+  {
+    title: '数据源类型',
+    dataIndex: 'data_source_type',
+    key: 'data_source_type'
+  },
+  // {
+  //   title: '创建者',
+  //   dataIndex: 'creator',
+  //   key: 'creator'
+  //   // width: 200
+  // },
+  {
+    title: '创建时间',
+    dataIndex: 'created_at',
+    key: 'created_at',
+    // width: 200,
+    sortable: {
+      sortDirections: ['ascend', 'descend']
+    }
+  },
+
+  {
+    title: '操作',
+    dataIndex: 'action',
+    key: 'action'
+    // width: 200
+  }
+]);
+
+const page = ref({
+  total: 0,
+  current: 1,
+  page_size: 20
+});
+const is_Loading = ref(false);
+const groupTemplates = ref([]);
+
+const is_loading_status = ref(false);
+
+const changePageChange = (current) => {
+  page.value.current = current;
+  getDataSourceListFun();
+};
+const changePageSizeChange = (page_size) => {
+  page.value.page_size = page_size;
+  getDataSourceListFun();
+};
+
+const getDataSourceListFun = async () => {
+  is_Loading.value = true;
+  try {
+    const res = await getDataSourceList({
+      page: page.value.current,
+      size: page.value.page_size,
+      metric_name: filter_config.value.filter_value || ''
+    });
+    if (res.code / 1 === 200) {
+      console.log(res);
+      groupTemplates.value = res.data || [];
+      page.value.total = res.pages.total || 0;
+    }
+  } catch (error) {
+    console.log(error);
+  } finally {
+    is_Loading.value = false;
+  }
+};
+const handleDelete = async (record) => {
+  console.log(record);
+  selected_id_list.value = [record.id];
+  show_delete_modal.value = true;
+};
+// 批量删除
+const deleteSelected = async () => {
+  if (selectedKeys.value.length === 0) {
+    Message.warning('请选择要删除的草稿');
+    return;
+  }
+  selected_id_list.value = selectedKeys.value;
+  if (selected_id_list.value.length > 0) {
+    is_branch.value = true;
+  } else {
+    is_branch.value = false;
+  }
+  show_delete_modal.value = true;
+};
+
+const handleConfirm = async () => {
+  try {
+    const res = await deleteDataSourceItem({
+      metric_id: selected_id_list.value
+    });
+
+    if (res.code / 1 === 200) {
+      Message.success('删除成功');
+      getDataSourceListFun();
+    } else {
+      Message.error('删除失败');
+    }
+  } catch (error) {
+    console.log(error);
+    Message.error('删除失败');
+  } finally {
+    selected_id_list.value = [];
+    selectedKeys.value = [];
+    show_delete_modal.value = false;
+    is_branch.value = false;
+  }
+};
+const handleCancel = () => {
+  show_delete_modal.value = false;
+  selected_id_list.value = [];
+  selectedKeys.value = [];
+  is_branch.value = false;
+};
+const handleStatusChange = (record) => {
+  console.log(record);
+  is_loading_status.value = true;
+  editTemplateItemFunc({ id: record.id, status: record.status });
+};
+
+const editTemplateItemFunc = async (item) => {
+  try {
+    const res = await editTemplateItem({
+      ...item
+    });
+    if (res.code / 1 === 200) {
+      // Message.success('编辑成功');
+      getDataSourceListFun();
+    } else {
+      // Message.error('编辑失败');
+    }
+  } catch (error) {
+    console.log(error);
+    // Message.error('编辑失败');
+  } finally {
+    is_loading_status.value = false;
+  }
+};
+
+const handleEditRemark = (record) => {
+  record.temp_remark = record.remark;
+  record.is_remark_edit = true;
+  record.show_edit_btn = false;
+  nextTick(() => {
+    const input = document.querySelector('.remark-edit .arco-input');
+    if (input) {
+      input.focus();
+    }
+  });
+};
+
+const handleSaveRemark = async (record) => {
+  try {
+    const res = await editTemplateItem({
+      id: record.id,
+      remark: record.temp_remark
+    });
+    if (res.code / 1 === 200) {
+      Message.success('保存成功');
+      record.remark = record.temp_remark;
+      record.is_remark_edit = false;
+      delete record.temp_remark;
+    } else {
+      Message.error('保存失败');
+    }
+  } catch (error) {
+    console.log(error);
+    Message.error('保存失败');
+  }
+};
+
+const handleCancelRemark = (record) => {
+  record.is_remark_edit = false;
+  delete record.temp_remark;
+};
+
+const handleEdit = (record) => {
+  emit('edit-datasource', record);
+};
+
+const clearFilter = (params) => {
+  filter_config.value = {
+    filter: params.type,
+    filter_value: params.value
+  };
+  page.value.current = 1;
+};
+
+const toSearch = (params) => {
+  filter_config.value = {
+    filter: params.type,
+    filter_value: params.value
+  };
+  page.value.current = 1;
+
+  getDataSourceListFun();
+};
+onMounted(() => {
+  getDataSourceListFun();
+});
+
+defineExpose({
+  update: getDataSourceListFun,
+  deleteSelected: deleteSelected,
+  toSearch: toSearch,
+  clearFilter: clearFilter
+});
+</script>
+<style scoped lang="css">
+.detail-body {
+  position: relative;
+  height: calc(100% - 52px);
+  padding: 0 20px;
+  z-index: 10;
+}
+.column {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+}
+.column .link {
+  color: #3d3d3d;
+  cursor: pointer;
+}
+.column .link_del {
+  color: #e34d59;
+}
+:deep(.arco-table-element th) {
+  background-color: var(--table-th-bg) !important;
+}
+:deep(.arco-checkbox-checked .arco-checkbox-icon) {
+  background: var(--primary-default);
+}
+:deep(.arco-checkbox-indeterminate .arco-checkbox-icon) {
+  background: var(--primary-default);
+}
+.pagination {
+  margin-top: 10px;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+}
+:deep(.arco-pagination-item-active, .arco-pagination-item-active:hover) {
+  color: var(--primary-default);
+  background: var(--title-bg);
+}
+
+.remark-cell {
+  position: relative;
+}
+
+.remark-text {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  width: 100%;
+}
+
+.edit-icon {
+  cursor: pointer;
+  color: var(--color-text-2);
+  opacity: 0;
+  transition: opacity 0.2s;
+}
+
+.remark-cell:hover .edit-icon {
+  opacity: 1;
+}
+
+.edit-icon:hover {
+  color: var(--primary-default);
+}
+
+.remark-edit {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  gap: 8px;
+  width: 100%;
+}
+
+.edit-actions {
+  display: flex;
+  gap: 8px;
+}
+
+.edit-actions .arco-btn {
+  min-width: 60px;
+}
+.icon_edit_btn {
+  font-size: 16px;
+  cursor: pointer;
+}
+.icon_edit_btn:hover {
+  color: var(--primary-default);
+}
+</style>

+ 111 - 0
src/modules/data-source-manage/data-source/DataSourceHeader.vue

@@ -0,0 +1,111 @@
+<template>
+  <div class="detail-header">
+    <div class="left-search">
+      <a-select :style="{ width: '120px' }" v-model="search_type" placeholder="请选择">
+        <a-option v-for="item in search_types" :key="item.value" :value="item.value">{{ item.label }}</a-option>
+      </a-select>
+      <a-input-search
+        :style="{ width: '320px' }"
+        placeholder="请输入搜索内容"
+        v-model="search_value"
+        @keyup.enter="handleSearch"
+        @input="handleSearch"
+      />
+    </div>
+    <div class="right-tools">
+      <a-button class="tools_btn" type="primary" @click="handleDeleteSelected">x&nbsp;&nbsp;批量删除</a-button>
+      <a-button class="tools_btn" type="primary" @click="handleAddDataSource">+&nbsp;&nbsp;新增数据源</a-button>
+    </div>
+  </div>
+</template>
+
+<script setup lang="js">
+import { ref,watch } from 'vue'
+
+const emit = defineEmits(['addTemplate','deleteSelected','toSearch','clearFilter','addDataSource'])
+import {funcDebounce} from '../../../utils/utils'
+
+const search_types = [
+  {
+    label: '数据源名称',
+    value: 'metric_name'
+  },
+  // {
+  //   label: '数据源描述',
+  //   value: 'remark'
+  // },
+  // {
+  //   label: '创作者',
+  //   value: 'creator'
+  // }
+]
+const search_type = ref('metric_name')
+const search_value = ref('')
+
+const handleDeleteSelected = () => {
+  emit('deleteSelected')
+}
+const handleAddDataSource = () => {
+  emit('addDataSource')
+}
+const clearSearch = () => {
+  search_type.value = 'name'
+  search_value.value = ''
+  emit('clearFilter',{
+      type: null,
+    value: null
+  })
+  // emit('toSearch', {
+  //   type: null,
+  //   value: null
+  // })
+}
+const handleSearch = funcDebounce(() => {
+  emit('toSearch', {
+    type: search_type.value,
+    value: search_value.value
+  })
+}, 300)
+
+watch(()=>search_type.value,()=>{
+  search_value.value = ''
+  emit('toSearch', {
+    type: null,
+    value: null
+  })
+},{immediate:false})
+defineExpose({
+  clearSearch
+})
+</script>
+<style scoped lang="css">
+.detail-header {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  padding: 10px 20px;
+}
+.left-search {
+  display: flex;
+  flex-direction: row;
+  gap: 20px;
+}
+.right-tools {
+  display: flex;
+  flex-direction: row;
+  gap: 20px;
+}
+.right-tools .tools_btn {
+  border: 1px solid var(--primary-default);
+  background: #fff;
+  color: var(--primary-default);
+  border-radius: 10px;
+}
+.right-tools .tools_btn:hover {
+  border: 1px solid var(--primary-default);
+  background: #fff;
+  color: var(--primary-default);
+  border-radius: 10px;
+  opacity: 0.6;
+}
+</style>

+ 15 - 0
src/modules/report-template/TemplateDetail.vue

@@ -16,6 +16,7 @@
         :common_config="common_config"
         @change="handleChange"
         @mention-click="handleMentionClick"
+        @chart-click="handleChartClick"
         @close-filter-model="closeFilterModel"
         @to-save-setting="handleToSaveSetting"
         @submit-html="handleSubmitHtml"
@@ -27,6 +28,7 @@
       <FilterModel
         v-if="show_filter_model"
         ref="filterModelRef"
+        :filter_type="target_filter_type"
         :filter_option="target_filter_option"
         :target_common_config_info="target_common_config_info"
         @content-changed="handleContentChanged"
@@ -95,6 +97,7 @@ const emit = defineEmits(['save-template', 'update-draft', 'send-report']);
 
 const show_filter_model = ref(false);
 const target_filter_option = ref(null);
+const target_filter_type = ref('mention');
 const is_metrics_loading = ref(false); // 指标列表加载中
 const data_source_list = ref([]); // 数据源列表
 const metrics_list = ref([]); // 度量列表
@@ -135,6 +138,18 @@ const handleMentionClick = (option) => {
   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;
+    show_filter_model.value = true;
+    target_filter_type.value = 'mention';
+  });
+};
+const handleChartClick = (option) => {
+  console.log(option, 890);
+  target_source_id.value = option.chartId || '';
+  // console.log(target_source_id.value);
+  target_filter_option.value = null;
+  target_filter_type.value = 'chart';
   nextTick(() => {
     target_filter_option.value = option;
     show_filter_model.value = true;

+ 1 - 1
src/modules/report-template/TemplateEditor.vue

@@ -3,7 +3,7 @@
     <div class="container">
       <TemplateHeader @toBack="toBack"></TemplateHeader>
       <TemplateDetail
-        :md_data="template_content"
+        :md_data="str_md"
         :template_name="template_name"
         :template_id="template_id"
         :draft_id="draft_id"

+ 1 - 0
src/modules/report-template/editor-chart/TableChart.vue

@@ -52,6 +52,7 @@ const currentIsLoading = computed(() => {
   return chartDataStore.value[props.chartId]?.isLoading || false;
 });
 </script>
+
 <style scoped lang="css">
 .table-render {
   width: 100%;

+ 3 - 1
src/modules/report-template/editor-model/CommonDimension.vue

@@ -3,9 +3,11 @@
     <div class="common-dimension-items">
       <div class="item">
         <span>时间范围:</span>
+        <!-- show-time
+          :time-picker-props="{ defaultValue: [`00`, `09`] }"
+          format="YYYY-MM-DD HH" -->
         <a-range-picker
           style="width: 380px"
-          format="YYYY-MM-DD"
           v-model="time_range"
           @change="handleTimeRangeChange"
           @clear="handleClearTimeRange"

+ 0 - 1453
src/modules/report-template/editor-model/EditorDetail - 副本.vue

@@ -1,1453 +0,0 @@
-<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" @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>
-    </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"
-      @action="handleAction"
-    />
-    <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 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
-});
-
-const emit = defineEmits(['update:modelValue', 'mention-click', 'close-filter-model', 'to-save-setting', 'submitHtml']);
-
-// ============ 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 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);
-    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);
-    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);
-    immediateSave();
-    updateToolbar();
-  });
-};
-
-// ============ 输入监听(原有)===========
-const onInput = () => {
-  if (editorRef.value && !isRestoring) {
-    emit('submitHtml', editorRef.value.innerHTML);
-    emit('update:modelValue', editorRef.value.innerHTML);
-    debouncedSave();
-    syncMentionsData();
-  }
-};
-
-// ============ 外部数据更新监听 ============
-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();
-
-  dropdownPosition.value = {
-    top: rect.bottom - containerRect.top + window.scrollY,
-    left: rect.left - containerRect.left + window.scrollX
-  };
-
-  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.metric_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);
-}
-// 当编辑器内容改变时,同步数据
-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]');
-  allMentions.forEach((mention) => {
-    mention.classList.remove('mention-highlighted');
-  });
-  // 关闭过滤器
-  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));
-    target_action.value = JSON.parse(option.option.advanced_computing_config);
-    // 给父组件一个触发一个事件,显示过滤器
-    emit('mention-click', option);
-    nextTick(() => {
-      currentMentionElement.value = mention;
-
-      const rect = mention.getBoundingClientRect();
-      const containerRect = containerRef.value.getBoundingClientRect();
-
-      actionMenuPosition.value = {
-        top: rect.bottom - containerRect.top + window.scrollY,
-        left: rect.left - containerRect.left + window.scrollX
-      };
-
-      nextTick(() => {
-        showActionMenu.value = true;
-      });
-      e.stopPropagation();
-    });
-  }
-}
-const closeFilterModel = () => {
-  emit('close-filter-model');
-};
-
-const toSaveSetting = () => {
-  showTips.value = false;
-  emit('to-save-setting');
-};
-const toNotSaveSetting = () => {
-  showTips.value = false;
-};
-
-// ============ 拖拽接收逻辑 ============
-const onDragOverMetric = (event) => {
-  // 可选:只允许特定类型拖入
-  event.preventDefault();
-  event.stopPropagation();
-  // if (event.dataTransfer.types.includes('application/x-metrics')) {
-  //   event.dataTransfer.dropEffect = 'copy';
-  // } else {
-  //   event.dataTransfer.dropEffect = 'none';
-  // }
-  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 (editorRef.value && document.activeElement !== editorRef.value) {
-    editorRef.value.focus({ preventScroll: true });
-  }
-
-  const text = event.dataTransfer.getData('text/plain');
-  if (!text?.startsWith('__METRIC__')) return;
-
-  let item;
-  try {
-    item = JSON.parse(text.slice('__METRIC__'.length));
-    if (!item || !item.metric_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);
-}
-
-// 处理图表面板下拉菜单点击-插入图表节点
-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.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,
-      chartData: chartDataStore.value[chartId].data,
-      chartColumns: chartDataStore.value[chartId].columns,
-      isLoading: chartDataStore.value[chartId].isLoading
-    });
-    // 挂载到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);
-}
-
-// 更新图表数据的示例函数
-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; /* 👈 关键:使浮层定位基于此 */
-}
-
-.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);
-  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);
-}
-
-.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>

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

@@ -68,15 +68,15 @@
       <!-- 右侧:插入图表等扩展功能 -->
       <div class="toolbar_right">
         <div class="chat-select" ref="chartButtonRef">
-          <a-button class="insert_btn" v-if="false" @click="toggleChartPanelDropdown">
+          <a-button class="insert_btn" v-if="true" @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>
+        <div>
           <button @click="insertData">插入数据</button>
-        </div> -->
+        </div>
       </div>
     </div>
     <div class="common-config">
@@ -122,17 +122,17 @@
       :direction="actionMenuDirection"
       @action="handleAction"
     />
-    <ChartPanelDropdown
+    <!-- <ChartPanelDropdown
       ref="chartPanelDropdownRef"
       :visible="showChartPanelDropdown"
       :position="chartPanelDropdownPosition"
-    />
-    <!-- <ChartPanelDropdown
+    /> -->
+    <ChartPanelDropdown
       ref="chartPanelDropdownRef"
       :visible="showChartPanelDropdown"
       :position="chartPanelDropdownPosition"
       @chart-click="handleChartPanelDropdownClick"
-    /> -->
+    />
   </div>
   <TipsModel v-model:visible="showTips" @ok="toSaveSetting" @cancel="toNotSaveSetting" />
 </template>

+ 192 - 10
src/modules/report-template/editor-model/EditorDetail copy.vue → 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">
@@ -92,7 +99,7 @@
       @blur="flushPendingSave"
       @mouseup="updateToolbar"
       @keyup="updateToolbar"
-      @keydown="handleKeydownForMention"
+      @keydown="handleKeydown"
       @click="handleEditorClick"
     ></div>
 
@@ -433,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];
@@ -589,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;
@@ -804,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);
@@ -885,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;
 }
@@ -1124,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);
@@ -1442,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;
@@ -1550,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 {
@@ -1598,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;
 }
 
 /* 指标高亮样式 */
@@ -1642,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;
 }
 

+ 202 - 38
src/modules/report-template/editor-select/MentionActionMenu.vue

@@ -23,19 +23,84 @@
         <path d="M6 12l4-4-4-4z" />
       </svg>
 
-      <!-- 二级子菜单 -->
-      <div v-if="openSubMenuKey === 'calc'" class="submenu" :class="{ 'submenu-up': direction === 'up' }">
-        <button
-          v-for="child in actionMenuItems.top_n_config.children"
-          :key="child.name"
-          type="button"
-          class="submenu-item"
-          :class="{ 'is-active': currentConfig.top_n === child.value }"
-          :disabled="!child.is_show"
-          @click.stop="handleChildClick(child, { key: 'top_n', label: '高级计算' })"
+      <!-- 二级子菜单:TopN 和同环比 -->
+      <div
+        v-if="openSubMenuKey === 'calc' || openSubMenuKey === 'topn' || openSubMenuKey === 'yoymom'"
+        class="submenu"
+        :class="{ 'submenu-up': direction === 'up' }"
+        @mouseenter="onMenuEnter"
+        @mouseleave="onMenuLeave"
+      >
+        <!-- TopN 子菜单 -->
+        <div
+          class="submenu-item has-children"
+          v-if="actionMenuItems.top_n_config"
+          @mouseenter="
+            () => {
+              openSubmenu('topn');
+            }
+          "
         >
-          {{ child.name }}
-        </button>
+          <span>TopN</span>
+          <svg class="submenu-arrow" viewBox="0 0 16 16" width="12" height="12" fill="#888">
+            <path d="M6 12l4-4-4-4z" />
+          </svg>
+          <!-- 三级子菜单:TopN 选项 -->
+          <div
+            v-if="openSubMenuKey === 'topn'"
+            class="submenu"
+            :class="{ 'submenu-up': direction === 'up' }"
+            @mouseenter="onMenuEnter"
+            @mouseleave="onMenuLeave"
+          >
+            <button
+              v-for="child in actionMenuItems.top_n_config.children"
+              :key="child.name"
+              type="button"
+              class="submenu-item"
+              :class="{ 'is-active': currentConfig.top_n === child.value }"
+              :disabled="!child.is_show"
+              @click.stop="handleChildClick(child, { key: 'top_n', label: 'TopN' })"
+            >
+              {{ child.name }}
+            </button>
+          </div>
+        </div>
+        <!-- 同环比子菜单 -->
+        <div
+          class="submenu-item has-children"
+          v-if="actionMenuItems.yoy_or_mom_config"
+          @mouseenter="
+            () => {
+              openSubmenu('yoymom');
+            }
+          "
+        >
+          <span>同环比</span>
+          <svg class="submenu-arrow" viewBox="0 0 16 16" width="12" height="12" fill="#888">
+            <path d="M6 12l4-4-4-4z" />
+          </svg>
+          <!-- 三级子菜单:同环比选项 -->
+          <div
+            v-if="openSubMenuKey === 'yoymom'"
+            class="submenu"
+            :class="{ 'submenu-up': direction === 'up' }"
+            @mouseenter="onMenuEnter"
+            @mouseleave="onMenuLeave"
+          >
+            <button
+              v-for="child in actionMenuItems.yoy_or_mom_config.children"
+              :key="child.name"
+              type="button"
+              class="submenu-item"
+              :class="{ 'is-active': currentConfig.yoy_or_mom === child.value }"
+              :disabled="!child.is_show"
+              @click.stop="handleChildClick(child, { key: 'yoy_or_mom', label: '同环比' })"
+            >
+              {{ child.name }}
+            </button>
+          </div>
+        </div>
       </div>
     </div>
 
@@ -100,6 +165,37 @@
         </button>
       </div>
     </div>
+
+    <!-- 聚合方式 -->
+    <div
+      v-if="actionMenuItems.aggregation_config && actionMenuItems.aggregation_config.is_show"
+      class="menu-item has-children"
+      @mouseenter="
+        () => {
+          openSubmenu('aggregation');
+        }
+      "
+    >
+      <span>聚合方式</span>
+      <svg class="submenu-arrow" viewBox="0 0 16 16" width="12" height="12" fill="#888">
+        <path d="M6 12l4-4-4-4z" />
+      </svg>
+
+      <!-- 二级子菜单 -->
+      <div v-if="openSubMenuKey === 'aggregation'" class="submenu" :class="{ 'submenu-up': direction === 'up' }">
+        <button
+          v-for="child in actionMenuItems.aggregation_config.children"
+          :key="child.name"
+          type="button"
+          class="submenu-item"
+          :class="{ 'is-active': currentConfig.aggregation_type === child.value }"
+          :disabled="!child.is_show"
+          @click.stop="handleChildClick(child, { key: 'aggregation_type', label: '聚合方式' })"
+        >
+          {{ child.name }}
+        </button>
+      </div>
+    </div>
   </div>
   <CustomModal
     v-if="show_custom_modal"
@@ -134,31 +230,6 @@ const actionMenuItems = ref({
   sort_type: 'desc',
   top_bottom_n: 3,
   limit_type: 'top',
-  top_n_config: {
-    is_show: 1,
-    children: [
-      {
-        name: 'top3',
-        value: 3,
-        is_show: 1
-      },
-      {
-        name: 'top5',
-        value: 5,
-        is_show: 1
-      },
-      {
-        name: 'top10',
-        value: 10,
-        is_show: 1
-      },
-      {
-        name: '自定义',
-        value: null,
-        is_show: 1
-      }
-    ]
-  },
   sort_config: {
     is_show: 1,
     children: [
@@ -239,6 +310,22 @@ const actionMenuItems = ref({
       }
     ]
   },
+  yoy_or_mom_config: {
+    is_show: 1,
+    children: [
+      {
+        name: '同比',
+        value: 'yoy',
+        is_show: 1
+      },
+      {
+        name: '环比',
+        value: 'mom',
+        is_show: 1
+      }
+    ]
+  },
+
   sort_config: {
     is_show: 1,
     children: [
@@ -254,6 +341,7 @@ const actionMenuItems = ref({
       }
     ]
   },
+
   format_config: {
     is_show: 1,
     children: [
@@ -293,12 +381,43 @@ const actionMenuItems = ref({
         is_show: 1
       }
     ]
+  },
+  aggregation_config: {
+    is_show: 1,
+    children: [
+      {
+        name: '求和',
+        value: 'sum',
+        is_show: 1
+      },
+      {
+        name: '求平均',
+        value: 'avg',
+        is_show: 1
+      },
+      {
+        name: '求最大',
+        value: 'max',
+        is_show: 1
+      },
+      {
+        name: '求最小',
+        value: 'min',
+        is_show: 1
+      },
+      {
+        name: '求总数',
+        value: 'count',
+        is_show: 1
+      }
+    ]
   }
 });
 const currentConfig = ref({
   sort_type: 'desc',
   format_type: 'auto',
-  top_n: 3
+  top_n: 3,
+  aggregation_type: 'sum'
 });
 
 const openSubMenuKey = ref(null);
@@ -385,6 +504,38 @@ watch(
   () => props.actions,
   (actions) => {
     actionMenuItems.value = { ...actions };
+    actionMenuItems.value.aggregation_config = {
+      is_show: 1,
+      children: [
+        {
+          name: '求和',
+          value: 'sum',
+          is_show: 1
+        },
+        {
+          name: '求平均',
+          value: 'avg',
+          is_show: 1
+        },
+        {
+          name: '求最大',
+          value: 'max',
+          is_show: 1
+        },
+        {
+          name: '求最小',
+          value: 'min',
+          is_show: 1
+        },
+        {
+          name: '求总数',
+          value: 'count',
+          is_show: 1
+        }
+      ]
+    };
+
+    console.log(actionMenuItems.value, 88888);
   }
 );
 
@@ -477,4 +628,17 @@ watch(
   background-color: var(--primary-default);
   color: white;
 }
+
+/* 三级菜单样式 */
+.submenu-item.has-children {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 6px 12px;
+  cursor: pointer;
+}
+
+.submenu-item.has-children:hover {
+  background-color: #e6f7ff;
+}
 </style>

+ 0 - 0
src/modules/report-template/editor-select - 副本/ChartPanelDropdown.vue → src/modules/report-template/editor-select_备份_无同环比/ChartPanelDropdown.vue


+ 0 - 0
src/modules/report-template/editor-select - 副本/CustomModal.vue → src/modules/report-template/editor-select_备份_无同环比/CustomModal.vue


+ 11 - 4
src/modules/report-template/editor-select - 副本/MentionActionMenu.vue → src/modules/report-template/editor-select_备份_无同环比/MentionActionMenu.vue

@@ -24,7 +24,7 @@
       </svg>
 
       <!-- 二级子菜单 -->
-      <div v-if="openSubMenuKey === 'calc'" class="submenu">
+      <div v-if="openSubMenuKey === 'calc'" class="submenu" :class="{ 'submenu-up': direction === 'up' }">
         <button
           v-for="child in actionMenuItems.top_n_config.children"
           :key="child.name"
@@ -55,7 +55,7 @@
       </svg>
 
       <!-- 二级子菜单 -->
-      <div v-if="openSubMenuKey === 'sort'" class="submenu">
+      <div v-if="openSubMenuKey === 'sort'" class="submenu" :class="{ 'submenu-up': direction === 'up' }">
         <button
           v-for="child in actionMenuItems.sort_config.children"
           :key="child.name"
@@ -86,7 +86,7 @@
       </svg>
 
       <!-- 二级子菜单 -->
-      <div v-if="openSubMenuKey === 'format'" class="submenu">
+      <div v-if="openSubMenuKey === 'format'" class="submenu" :class="{ 'submenu-up': direction === 'up' }">
         <button
           v-for="child in actionMenuItems.format_config.children"
           :key="child.name"
@@ -121,7 +121,8 @@ const props = defineProps({
   visible: Boolean,
   position: { type: Object, default: () => ({ top: 0, left: 0 }) },
   actions: Object,
-  actionConfig: Object
+  actionConfig: Object,
+  direction: { type: String, default: 'down' } // 'down' 或 'up',控制菜单方向
 });
 
 const emit = defineEmits(['action', 'close']);
@@ -451,6 +452,12 @@ watch(
   z-index: 1002;
 }
 
+/* 向上显示的二级子菜单 */
+.submenu.submenu-up {
+  top: auto;
+  bottom: 0;
+}
+
 .submenu-item {
   display: block;
   width: 100%;

+ 8 - 9
src/modules/report-template/editor-select - 副本/MentionDropdown.vue → src/modules/report-template/editor-select_备份_无同环比/MentionDropdown.vue

@@ -7,15 +7,13 @@
     :style="{ top: position.top + 'px', left: position.left + 'px' }"
     @click.stop
   >
-    <button
-      v-for="(opt, index) in options"
-      :key="index"
-      @click="handleSelect(opt)"
-      class="dropdown-option"
-      :class="{ active: index === selectedIndex }"
-    >
-      {{ opt.metric_name }}
-    </button>
+    <template v-for="(opt, index) in options" :key="index">
+      <a-tooltip :content="`数据源:${opt.source.metric_name}`" position="right">
+        <button @click="handleSelect(opt)" class="dropdown-option" :class="{ active: index === selectedIndex }">
+          {{ opt.field_name }}
+        </button>
+      </a-tooltip>
+    </template>
   </div>
 </template>
 
@@ -86,6 +84,7 @@ watch(
   min-width: 120px;
   max-height: 200px;
   overflow-y: auto;
+  padding: 10px;
 }
 
 .dropdown-option {

+ 30 - 0
src/modules/report-template/filter-model/ChartFilter.vue

@@ -0,0 +1,30 @@
+<template>
+  <div class="chart_filter">
+    <div class="header">
+      <div class="title">明细表</div>
+    </div>
+  </div>
+</template>
+
+<script setup >
+</script>
+<style scoped lang="css">
+.chart_filter {
+  height: 100%;
+  border-bottom: 1px solid #d8d8d8;
+}
+.header {
+  display: flex;
+  justify-content: space-between;
+  padding: 0px 15px;
+  height: 50px;
+  line-height: 50px;
+  border-bottom: 1px solid #ebeef5;
+}
+.title {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  width: 100%;
+}
+</style>

+ 24 - 5
src/modules/report-template/filter-model/MentionFilter.vue

@@ -7,11 +7,18 @@
       <div class="measure">
         <div class="title"><span style="color: red">*</span>度量</div>
         <div class="measure-list">
-          <div class="measure-item">
-            <img :src="MetricsIcon" alt="" />
-            <span>{{ measure_name }}</span>
-            <!-- <icon-caret-down class="item-end-icon" /> -->
-          </div>
+          <a-popover>
+            <div class="measure-item">
+              <img :src="MetricsIcon" alt="" />
+              <span>{{ measure_name }}</span>
+              <span v-if="measure_aggregate">({{ measure_aggregate }})</span>
+              <!-- <icon-caret-down class="item-end-icon" /> -->
+            </div>
+            <template #content>
+              <p style="font-size: 14px; color: #666">度量名称:{{ measure_name }}</p>
+              <p style="font-size: 14px; color: #666" v-if="measure_aggregate">聚合方式:{{ measure_aggregate }}</p>
+            </template>
+          </a-popover>
         </div>
       </div>
       <div class="dimension">
@@ -112,12 +119,24 @@ const props = defineProps({
 });
 
 const emit = defineEmits(['content-changed']);
+
+const aggregation_map = {
+  sum: '求和',
+  avg: '求平均值',
+  max: '求最大值',
+  min: '求最小值',
+  count: '计数'
+};
+
 const filter_title = computed(() => {
   return '指标 — ' + props.filter_option.option.field_name;
 });
 const measure_name = computed(() => {
   return props.filter_option.option.field_name;
 });
+const measure_aggregate = computed(() => {
+  return aggregation_map[reportEditor.QueryTargetAggregationType] || '求和';
+});
 
 const dimension_list = ref([]);
 

+ 1 - 0
src/modules/report-template/template-detail/EditorModel.vue

@@ -49,6 +49,7 @@ const props = defineProps({
 const emit = defineEmits([
   'change',
   'mention-click',
+  'chart-click',
   'close-filter-model',
   'to-save-setting',
   'submit-html',

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

@@ -7,12 +7,20 @@
       :target_common_config_info="target_common_config_info"
       @content-changed="handleContentChanged"
     ></MentionFilter>
+    <ChartFilter
+      ref="chartFilterRef"
+      v-if="type == 'chart'"
+      :filter_option="filter_option"
+      :target_common_config_info="target_common_config_info"
+      @content-changed="handleContentChanged"
+    ></ChartFilter>
   </div>
 </template>
 
 <script setup >
 import { ref, computed } from 'vue';
 import MentionFilter from '../filter-model/MentionFilter.vue';
+import ChartFilter from '../filter-model/ChartFilter.vue';
 
 const props = defineProps({
   filter_type: {

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

@@ -79,7 +79,7 @@ const clearForm = () => {
     name: '',
     description: '',
   };
-  emit('close')
+  emit('cancel')
 };
 
 const addTemplateFunc = async (data) => {

+ 21 - 0
src/views/About.vue

@@ -13,6 +13,11 @@
   <div class="chart-container">
     <TableChart></TableChart>
   </div>
+  <a-select v-model="formData.data_source_type" @change="handleTypeChange">
+    <a-option value="1">mysql</a-option>
+    <a-option value="2">postgresql</a-option>
+    <!-- <a-option v-for="item in options" :key="item.value" :value="item.value">{{ item.label }}</a-option> -->
+  </a-select>
 </template>
 
 <script setup >
@@ -28,6 +33,22 @@ import Bar_1 from '../assets/chat-report/柱状图.png';
 import Line_1 from '../assets/chat-report/折线图.png';
 import bar_line from '../assets/chat-report/组合图.png';
 import TableChart from '../modules/report-template/editor-chart/TableChart.vue';
+
+const options = [
+  {
+    value: '1',
+    label: 'API接口'
+  },
+  {
+    value: '2',
+    label: '数据库类型'
+  }
+];
+const data_source_type = ref('1');
+
+const formData = ref({
+  data_source_type: '1'
+});
 const html = ref(`
  </select><h1>国网四川省电力公司配网运营分析月报</h1><h1>国网四川省电力公司 设备管理部</h1><h1>配电处</h1><h1>(2025年7月)</h1><h2>一、重点工作指标情况</h2><h3>(一)供电可靠性</h3><h3>1.供电可靠率</h3><p>1-7月全省全口径供电可靠率99.85%;城网99.95%,
         农网99.83%。全口径平均停电时间7.53小时(同比增加1.62%),城网<span data-uid="xxxxxx" data-mention="true">{{指标数据}}<span>平均停电时间2.39小时(同比增加49.38%)、农网平均停电时间8.4小时(同比增加1.08%)。</p>