Selaa lähdekoodia

feat:1.增加source_list,保存时减少数据量;2修复html转md格式错误;3.修复aggs_config,清空维度时删除里面的aggs_config所有配置;4.新增点击富文本中的指标时,替换新的uid和index;5.新增数据源配置日期精确到前天0点和24点,时间过滤器增加限制;6.新增数据源API接口数据源接口参数默认值字段;7.修复模板编辑中,过滤器常显示,富文本宽度固定,过滤器列,加入空状态,切换数据源时或者不选中指标时,显示为空;8.修改当前勾选全局取消基础中任何一项时,富文本中对于的指标标红处理,实时监控,实时更新;9.富文本中增加<special>输入框,处理数据提交和数据回显时的结构;10.增加分组,与维度关联,只有添加的维度才能添加到分组,且删除维度时对应的维度也随之删除,分组只能添加一个等

韩洋 3 kuukautta sitten
vanhempi
sitoutus
cc8a1525ec

BIN
src.zip


+ 5 - 1
src/3rd-libs/turndown.js

@@ -1,5 +1,9 @@
 import TurndownService from 'turndown'
 
-const turndownService = new TurndownService()
+const turndownService = new TurndownService({
+  headingStyle: 'atx',
+  linkStyle: 'inlined', // 链接:inlined / referenced
+  linkReferenceStyle: 'full'
+})
 
 export default turndownService

+ 1 - 0
src/assets/chat-report/copy.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="14.999834060668945" height="16.666667938232422" viewBox="0 0 14.999834060668945 16.666667938232422"><path d="M3.3331671,3.3333335L3.3331671,0.83333337C3.3331671,0.37310004,3.7062669,0,4.1665006,0L14.1665,0C14.62675,0,14.999834,0.37310004,14.999834,0.83333337L14.999834,12.5C14.999834,12.960251,14.62675,13.333334,14.1665,13.333334L11.6665,13.333334L11.6665,15.832584C11.6665,16.293253,11.291584,16.666668,10.827501,16.666668L0.8388834,16.666668C0.37549162,16.666668,0,16.296169,0,15.832584L0.0021666288,4.1673923C0.0022500753,3.7067587,0.37720007,3.3333335,0.8411833,3.3333335L3.3331671,3.3333335ZM4.9998331,3.3333335L11.6665,3.3333335L11.6665,11.666668L13.333167,11.666668L13.333167,1.6666667L4.9998331,1.6666667L4.9998331,3.3333335ZM3.3331671,7.5L3.3331671,9.1666679L8.3331661,9.1666679L8.3331661,7.5L3.3331671,7.5ZM3.3331671,10.833334L3.3331671,12.5L8.3331661,12.5L8.3331661,10.833334L3.3331671,10.833334Z" fill="#0A817E" fill-opacity="1" style="mix-blend-mode:passthrough"/></svg>

+ 1 - 0
src/assets/chat-report/paste.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="18" height="18" viewBox="0 0 18 18"><path d="M10.5,13.5L10.5,7.5L3,7.5L3,15L10.5,15L10.5,13.5ZM13.5,13.5L13.5,18L0,18L0,4.5L4.5,4.5L4.5,3L6,3L6,4.5L13.5,4.5L13.5,12L15,12L15,13.5L13.5,13.5ZM4.5,0L6,0L6,1.5L4.5,1.5L4.5,0ZM7.5,0L9,0L9,1.5L7.5,1.5L7.5,0ZM10.5,0L12,0L12,1.5L10.5,1.5L10.5,0ZM13.5,0L15,0L15,1.5L13.5,1.5L13.5,0ZM16.5,0L18,0L18,1.5L16.5,1.5L16.5,0ZM16.5,3L18,3L18,4.5L16.5,4.5L16.5,3ZM16.5,6L18,6L18,7.5L16.5,7.5L16.5,6ZM16.5,9L18,9L18,10.5L16.5,10.5L16.5,9ZM16.5,12L18,12L18,13.5L16.5,13.5L16.5,12ZM7.5,12L9,12L9,13.5L7.5,13.5L7.5,12ZM4.5,12L6,12L6,13.5L4.5,13.5L4.5,12ZM4.5,9L6,9L6,10.5L4.5,10.5L4.5,9Z" fill="#0A817E" fill-opacity="1" style="mix-blend-mode:passthrough"/></svg>

+ 92 - 12
src/base/store/use-report-editor.js

@@ -7,12 +7,14 @@ import { defineStore } from 'pinia';
 export const useReportEditor = defineStore('report-editor', {
   state: () => {
     return {
+      source_list: [], //接口返回的最新的全部数据源列表
       metrics_list: [], // 编辑器中添加到指标数据
       common_config: {
         common_start_time: '', // 公共维度-时间范围-开始时间
         common_end_time: '', // 公共维度-时间范围-结束时间
         common_area: '', // 公共维度-区域
       }, // 公共维度配置放在这里
+      used_source_list: [], // 编辑器中添加的指标所属的数据源都放在这里  option中metric_id与数据源的id对应
       existing_match_list: [],// 存在的度量数据,用于匹配富文本中的指标
       target_metric_id: null, // 当前选中的指标数据
       target_report_markdown: '',// 当前富文本中内容md格式
@@ -24,6 +26,14 @@ export const useReportEditor = defineStore('report-editor', {
     };
   },
   actions: {
+    // 更新source_list
+    updateSourceList(list) {
+      this.source_list = list;
+    },
+    // 更新使用到的source_list
+    updateUsedSourceList(list) {
+      this.used_source_list = list;
+    },
     setCommonConfig(key, value) {
       this.common_config[key] = value;
     },
@@ -53,7 +63,7 @@ export const useReportEditor = defineStore('report-editor', {
       } else {
         const map_metric = this.mapMetric(metric)
         this.metrics_list.push(map_metric);
-        console.log('xxx:添加指标数据1', this.metrics_listic);
+        // console.log('xxx:添加指标数据1', this.metrics_listic);
       }
 
     },
@@ -67,18 +77,21 @@ export const useReportEditor = defineStore('report-editor', {
 
       if (list.length) {
         const new_list = this.syncArraysById(this.metrics_list, list)
-        console.log('xxx:添加指标数据3', new_list);
+        // console.log('xxx:添加指标数据3', new_list);
         this.metrics_list = new_list.map(item => {
           return this.mapMetric(item)
         })
-        console.log('xxx:添加指标数据2', this.metrics_list);
+        // 更新used_source_list, 从metrics_list中提取所有用到的source_id而后从数据源中去获取对应的source信息
+        // this.used_source_list = this.updateUsedSourceList(this.metrics_list);
+
+        // console.log('xxx:添加指标数据2', this.metrics_list);
       } else {
         this.metrics_list = [];
       }
     },
     // 编辑模板时获取指标数据赋值
     setMetricsList(list) {
-      console.log('list赋值了', list)
+      // console.log('list赋值了', list)
       this.metrics_list = [...list]
 
     },
@@ -152,6 +165,34 @@ export const useReportEditor = defineStore('report-editor', {
       }
       return old_list;
     },
+    // 更新source_list
+    updateUsedSourceList(list) {
+      // const uniqueMap = new Map();
+      // list.forEach(item => {
+      //   const source_id = item.option.metric_id;
+      //   if (source_id !== undefined && !uniqueMap.has(source_id)) {
+      //     uniqueMap.set(source_id, item);
+      //   }
+      // })
+      // const uniqueList = Array.from(uniqueMap.values());
+      // console.log('xxx:更新source_list', uniqueList);
+
+      // const result = uniqueList.map(item2 => {
+      //   const metricId = item2.option.metric_id;
+
+      //   // 在 list1 中查找 id 匹配的项
+      //   const relatedItem1 = this.metrics_list.find(item1 => item1.id === metricId);
+
+      //   // 返回合并后的对象 (根据需求调整返回结构)
+      //   return {
+      //     ...item2, // 展开 list2 的原始数据
+      //     sourceData: relatedItem1 || null, // 将 list1 的匹配项放入新字段,如果没找到则为 null
+      //     // 或者如果你想扁平化合并:
+      //     // ...(relatedItem1 || {})
+      //   };
+      // });
+
+    },
     // 映射metrics_list中添加到数据,将每一项新增一个index=uid,metric_base_info:{ "metric_name": "指标名称", "metric_id": "oU12ESIeGAD5qCw", "data_source_type": "api/db}
     mapMetric(item) {
       const map_item = { ...item }
@@ -188,7 +229,7 @@ export const useReportEditor = defineStore('report-editor', {
         }
         map_item.api_config.need_fields.push(need_field)
         // map_item.api_config.need_fields.push({ name: 11111111, show_name: 2222222222 })
-        console.log('map_item.api_config', map_item)
+        // console.log('map_item.api_config', map_item)
         map_item.metric_config.sort_field_key = map_item.option.field_key || ''
         map_item.metric_config.default_metric_field_key = map_item.option.field_key || ''
         map_item.metric_config.format_field_key = map_item.option.field_key || ''
@@ -201,7 +242,7 @@ export const useReportEditor = defineStore('report-editor', {
     changeMetricConfig(config) {
       this.metrics_list.forEach(item => {
         if (item.index === this.target_metric_id) {
-          console.log('item.metric_config', item.metric_config)
+          // console.log('item.metric_config', item.metric_config)
           item.metric_config[config.name] = config.value
           // 当聚合方式发生变化时,同时更新对应的 aggs_config 中的 aggs_type
           if (config.name === 'aggregation_type' && item.aggs_config) {
@@ -220,15 +261,38 @@ export const useReportEditor = defineStore('report-editor', {
     changeDimensionListConfig(dimension_list) {
       this.metrics_list.forEach(item => {
         if (item.index === this.target_metric_id) {
-          if (item.aggs_config) {
+          // 如果dimension_list为空数组,删除aggs_config
+          if (!dimension_list || dimension_list.length === 0) {
+            delete item.aggs_config
+          } else if (item.aggs_config) {
             item.aggs_config.group_by_field_key = [...dimension_list.map(it => it.field_key)]
           } else {
             item.aggs_config = {
               aggs_type: item.metric_config.aggregation_type || '',
               aggs_field_key: item.option.field_key,
-              group_by_field_key: [],
+              group_by_field_key: [...dimension_list.map(it => it.field_key)],
             }
-            item.aggs_config.group_by_field_key = [...dimension_list.map(it => it.field_key)]
+          }
+        }
+      })
+    },
+
+    // 配置分组
+    changeGroupListConfig(group_item) {
+      this.metrics_list.forEach(item => {
+        if (item.index === this.target_metric_id) {
+          item.aggs_config.result_group_by = group_item.field_key || ''
+          item.aggs_config.top_n_group = group_item.top_n || ''
+        }
+      })
+    },
+    // 配置分组-删除后更新
+    deleteGroupListConfig() {
+      this.metrics_list.forEach(item => {
+        if (item.index === this.target_metric_id) {
+          if (item.aggs_config) {
+            delete item.aggs_config.result_group_by
+            delete item.aggs_config.top_n_group
           }
         }
       })
@@ -240,6 +304,12 @@ export const useReportEditor = defineStore('report-editor', {
         if (item.index === this.target_metric_id) {
           if (item.aggs_config) {
             item.aggs_config.group_by_field_key.push(dimension_item.field_key)
+            // 将维度的field_key和name字段添加到api_config中的need_fields中
+            // if (item.api_config.need_fields) {
+            //   item.api_config.need_fields.push({ name: dimension_item.field_key, show_name: dimension_item.field_name })
+            // } else {
+            //   item.api_config.need_fields = [{ name: dimension_item.field_key, show_name: dimension_item.field_name }]
+            // }
           } else {
             item.aggs_config = {
               aggs_type: item.metric_config.aggregation_type || '',
@@ -294,7 +364,7 @@ export const useReportEditor = defineStore('report-editor', {
               metricFields.forEach(field => {
                 // 检查field是否存在且is_required为1
                 if (field && field.is_required / 1 === 1) {
-                  console.log('field', field)
+                  // console.log('field', field)
                   // 检查metric_fields是否存在且为数组
                   let need_complete = false;
 
@@ -338,7 +408,6 @@ export const useReportEditor = defineStore('report-editor', {
 
                   // 如果上述情况需要完善,则检查common_config
                   if (need_complete) {
-                    console.log(11111111111)
                     if (shouldCheckCommonConfig) {
                       // 检查common_config是否存在且为对象
                       if (this.common_config && typeof this.common_config === 'object') {
@@ -359,7 +428,6 @@ export const useReportEditor = defineStore('report-editor', {
                         // 否则检测通过,不添加到complete_list
                       } else {
                         // common_config不存在或不是对象,添加到需要完善的配置数组
-                        console.log(field, 22222222222)
                         complete_list.push({
                           uid: item.uid,
                           source_name: item.option.source.metric_name || '',
@@ -422,6 +490,14 @@ export const useReportEditor = defineStore('report-editor', {
     QueryTargetDimensionListConfig: (state) => {
       return state.metrics_list.find(item => item.index === state.target_metric_id)?.aggs_config?.group_by_field_key || []
     },
+    // 获取当前指标配置的分组信息
+    QueryTargetGroupListConfig: (state) => {
+      return state.metrics_list.find(item => item.index === state.target_metric_id)?.aggs_config?.result_group_by || ""
+    },
+    // 获取当前指标配置分组中的top_n信息
+    QueryTargetTopNListConfig: (state) => {
+      return state.metrics_list.find(item => item.index === state.target_metric_id)?.aggs_config?.top_n_group || ''
+    },
     // 获取当前指标中配置的过滤器信息
     QueryTargetFilterListConfig: (state) => {
       return state.metrics_list.find(item => item.index === state.target_metric_id)?.metric_fields || []
@@ -458,5 +534,9 @@ export const useReportEditor = defineStore('report-editor', {
     QueryTargetAggregationType: (state) => {
       return state.metrics_list.find(item => item.index === state.target_metric_id)?.metric_config?.aggregation_type || ''
     },
+    // 获取当前选中指标的id
+    QueryTargetMetricId: (state) => {
+      return state.target_metric_id
+    },
   },
 });

+ 77 - 0
src/components/DeleteDimGroupModal .vue

@@ -0,0 +1,77 @@
+<template>
+  <a-modal v-model:visible="visible" :simple="true" :hideCancel="false" :modalClass="'arco-modal-delete-simple'">
+    <template #title>
+      <div class="title-box">
+        <icon-exclamation-circle-fill style="color: #f37500; font-size: 28px" />
+        <span>当前维度关联分组,删除后分组将同步删除,是否继续?</span>
+      </div>
+    </template>
+    <template #footer>
+      <a-button type="info" style="border: none" @click="handleCancel">取消</a-button>
+      <a-button class="update_btn" @click="handleOk">确定</a-button>
+    </template>
+  </a-modal>
+</template>
+
+<script setup>
+import { toRefs } from 'vue';
+
+const props = defineProps({
+  visible: Boolean
+});
+
+const { visible } = toRefs(props);
+const emit = defineEmits(['ok', 'cancel']);
+
+const handleOk = () => {
+  emit('ok');
+};
+
+const handleCancel = () => {
+  emit('cancel');
+};
+</script>
+
+<style scoped>
+.update_btn {
+  background-color: var(--primary-default);
+  color: #fff;
+}
+
+.update_btn:hover {
+  background-color: var(--primary-default);
+  color: #fff;
+  opacity: 0.8;
+}
+</style>
+
+<style>
+/* 通过modal方法生成的简单类型弹窗样式,主要用于删除弹窗 */
+.arco-modal-delete-simple {
+  padding: 16px;
+}
+
+.arco-modal-delete-simple .arco-modal-title-align-center {
+  justify-content: start;
+}
+
+.arco-modal-delete-simple .arco-modal-footer {
+  margin-top: 16px;
+  text-align: right;
+}
+
+.arco-modal-delete-simple .title-box {
+  display: flex;
+  align-items: center;
+}
+
+.arco-modal-delete-simple .title-box > span {
+  font-weight: 500;
+  margin-left: 8px;
+}
+
+.arco-modal-container {
+  width: 100%;
+  margin: auto;
+}
+</style>

+ 77 - 0
src/components/GroupReplaceModal.vue

@@ -0,0 +1,77 @@
+<template>
+  <a-modal v-model:visible="visible" :simple="true" :hideCancel="false" :modalClass="'arco-modal-delete-simple'">
+    <template #title>
+      <div class="title-box">
+        <icon-exclamation-circle-fill style="color: #f37500; font-size: 28px" />
+        <span>已存在分组,是否替换?</span>
+      </div>
+    </template>
+    <template #footer>
+      <a-button type="info" style="border: none" @click="handleCancel">取消</a-button>
+      <a-button class="update_btn" @click="handleOk">确定</a-button>
+    </template>
+  </a-modal>
+</template>
+
+<script setup>
+import { toRefs } from 'vue';
+
+const props = defineProps({
+  visible: Boolean
+});
+
+const { visible } = toRefs(props);
+const emit = defineEmits(['ok', 'cancel']);
+
+const handleOk = () => {
+  emit('ok');
+};
+
+const handleCancel = () => {
+  emit('cancel');
+};
+</script>
+
+<style scoped>
+.update_btn {
+  background-color: var(--primary-default);
+  color: #fff;
+}
+
+.update_btn:hover {
+  background-color: var(--primary-default);
+  color: #fff;
+  opacity: 0.8;
+}
+</style>
+
+<style>
+/* 通过modal方法生成的简单类型弹窗样式,主要用于删除弹窗 */
+.arco-modal-delete-simple {
+  padding: 16px;
+}
+
+.arco-modal-delete-simple .arco-modal-title-align-center {
+  justify-content: start;
+}
+
+.arco-modal-delete-simple .arco-modal-footer {
+  margin-top: 16px;
+  text-align: right;
+}
+
+.arco-modal-delete-simple .title-box {
+  display: flex;
+  align-items: center;
+}
+
+.arco-modal-delete-simple .title-box > span {
+  font-weight: 500;
+  margin-left: 8px;
+}
+
+.arco-modal-container {
+  width: 100%;
+  margin: auto;
+}
+</style>

+ 22 - 7
src/modules/chat-item/Answer.vue

@@ -46,8 +46,10 @@
             v-if="answer_status == 2"
             class="answer-actions-btns"
             :type="type"
+            :dialog="dialog"
             @copy="handleReportCopy"
             @edit="handleReportEdit"
+            @download="handleDwonload"
           />
         </DialogReportContent>
         <DialogReportOutline v-if="is_report_outline" :content="report_outline" :is_loading="is_content_loading">
@@ -220,11 +222,9 @@ const target_icon = computed(() => {
     return getIcon(props.target_agent_icon, 'a');
   }
 });
-console.log(themeStore.value, THEME_PARTY, props.target_agent_icon, 909);
+// console.log(themeStore.value, THEME_PARTY, props.target_agent_icon, 909);
 
 const report_content = computed(() => {
-  // console.log(props.dialog)
-
   const result_list = props.dialog.result_list;
   // console.log(result_list)
   // if (result_list) {
@@ -490,6 +490,13 @@ const handleReportEdit = () => {
     }
   }
 };
+
+const handleDwonload = () => {
+  if (props.dialog.record_id) {
+    // downloadReport({ record_id: props.dialog.record_id });
+    toDownload();
+  }
+};
 const handleSelect = (v) => {
   if (v / 1 === 1) {
     handleEditIcon();
@@ -539,7 +546,13 @@ const downloadMergeExcel = async () => {
 };
 
 const toDownload = () => {
-  const record_id = props.dialog.report_files.record_id;
+  let record_id;
+  if (props.dialog.report_files && props.dialog.report_files.record_id) {
+    record_id = props.dialog.report_files.record_id;
+  }
+  if (props.dialog?.record_id) {
+    record_id = props.dialog.record_id;
+  }
 
   if (record_id) {
     is_download_loading.value = true;
@@ -548,7 +561,9 @@ const toDownload = () => {
         const url = URL.createObjectURL(res.data);
         const link = document.createElement('a');
         link.href = url;
-        link.download = props.dialog.report_files.file_name;
+        // console.log('下载报告url', url);
+        link.download = props.dialog?.report_files?.file_name || `报告_${record_id}`;
+        // console.log('下载报告名称', link.download);
         link.click();
       })
       .catch((e) => {
@@ -582,8 +597,8 @@ const toDownloadExcel = () => {
 };
 const toUpdateOutLine = () => {
   let md_content = '';
-  console.log(props.dialog);
-  console.log(props.dialog.result_list);
+  // console.log(props.dialog);
+  // console.log(props.dialog.result_list);
 
   if (props.dialog?.result_list && props.dialog?.result_list[0]) {
     md_content = props.dialog?.result_list[0];

+ 27 - 0
src/modules/chat-item/DialogActions.vue

@@ -11,6 +11,11 @@
           <icon-edit size="18" @click="handleEdit" />
         </a-tooltip>
       </div>
+      <div class="action-btn-item" v-if="show_download">
+        <a-tooltip content="下载" position="top">
+          <icon-download size="18" @click="handleDwonload" />
+        </a-tooltip>
+      </div>
       <div class="action-btn-item" v-if="show_like">
         <a-tooltip content="点赞" position="top">
           <icon-thumb-up
@@ -72,6 +77,10 @@ const props = defineProps({
     type: String,
     default: 'znwd'
   },
+  dialog: {
+    type: Object,
+    default: () => ({})
+  },
 
   like_status: {
     type: String,
@@ -96,6 +105,17 @@ const show_edit = computed(() => {
   }
 });
 
+/**
+ * 下载按钮只有报告生成中,灵活模板有,因为没有名称
+ * */
+const show_download = computed(() => {
+  if (props.type === 'bgsc' && props.dialog?.record_id && !props.dialog?.report_files?.file_name) {
+    return true;
+  } else {
+    return false;
+  }
+});
+
 const show_like = computed(() => {
   return props.type === 'znws';
 });
@@ -120,6 +140,13 @@ const handleCopy = () => {
 const handleEdit = () => {
   emit('edit');
 };
+
+/**
+ * 下载
+ */
+const handleDwonload = () => {
+  emit('download');
+};
 /**
  * 重试
  */

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

@@ -178,9 +178,9 @@ export const deleteTemplateItem = async ({ template_id_list }) => {
   })
 }
 // 编辑模板
-export const editTemplateItem = async ({ id, name, remark, status, content, template_config, common_config, type = 1 }) => {
+export const editTemplateItem = async ({ id, name, remark, status, content, template_config, common_config, source_list, type = 1 }) => {
   const data = Object.fromEntries(
-    Object.entries({ id, name, remark, status, content, template_config, common_config, type }).filter(([key, value]) => {
+    Object.entries({ id, name, remark, status, content, template_config, common_config, source_list, type }).filter(([key, value]) => {
       if (key === 'status') return value !== undefined;
       return value;
     })

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

@@ -13,7 +13,7 @@ const svg_icon = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://ww
 // 将html转化为md格式
 export const formatHtmlToMd = (html) => {
   // 先寻找html中所有带有data-uid的span标签,并将其替换为[[text_uid]]
-  const processedHtml = html.replace(/<span[^>]*data-uid="([^"]+)"[^>]*>([\s\S]*?)<\/span>/g, (match, uid, innerText) => {
+  let processedHtml = html.replace(/<span[^>]*data-uid="([^"]+)"[^>]*>([\s\S]*?)<\/span>/g, (match, uid, innerText) => {
     // 提取innerText并去除可能的{{}}包裹
     let text = innerText.replace(/\{\{([^}]+)\}\}/g, '$1').trim();
     // 只保留&nbsp;之前的文本内容
@@ -21,6 +21,13 @@ export const formatHtmlToMd = (html) => {
     return `[[${text}_${uid}]]`;
   });
 
+  // 处理带有 data-special 属性的 input 标签,提取 value 值并拼接到<special></special>标签中
+  processedHtml = processedHtml.replace(/<input[^>]*data-special[^>]*value="([^"]+)"[^>]*\/?>/gi, (match, value) => {
+    return `(special)${value}(\/special)`;
+  });
+
+  console.log(processedHtml, 9999)
+
   let processMd = turndownService.turndown(processedHtml);
 
   // 处理processMd中的[[xxxx\_xxxxxxx]]或[[xxxx\\_xxxxxxx]]格式为[[xxxx_xxxxxxx]]
@@ -52,6 +59,13 @@ export const formatMdToHtml = (exist_metrics, md) => {
   // 先解析markdown为html
   let html = marked.parse(md);
 
+  // 处理 (special) 标签,转换为 input 元素
+  html = html.replace(/\(special\)([\s\S]*?)\(\/special\)/gi, (match, content) => {
+    const placeholderText = '请输入特别提示词,以此统一文本结构、规范输出样式';
+    return `<input type="text" data-special="true" value="${content.trim()}" placeholder="${placeholderText}" contenteditable="true" style="background-color: #fff3cd; border: 1px solid #ffeeba; border-radius: 4px; padding: 2px 8px; color: #856404; font-weight: 500; min-width: 360px; display: inline-block; outline: none; font-size: inherit; font-family: inherit; vertical-align: middle; box-sizing: border-box; margin-left:5px;"/>`;
+  });
+
+
   // 只转换存在的指标
   html = html.replace(/\[\[([^_]+?)_([^\]]+?)\]\]/g, (match, text, uid) => {
     // 检查指标名称是否存在
@@ -70,6 +84,7 @@ export const formatMdToHtml = (exist_metrics, md) => {
     // 不存在则返回原始文本
     return match;
   });
+  console.log(html, 99999)
 
   return html;
 };

+ 10 - 3
src/modules/data-source-manage/add-source/components/ApiConfig.vue

@@ -35,6 +35,12 @@
                 style="flex: 1; margin-right: 10px"
                 @input="handleParamChange"
               />
+              <!-- <a-input
+                v-model="param.default_value"
+                placeholder="默认值"
+                style="flex: 1; margin-right: 10px"
+                @input="handleParamChange"
+              /> -->
               <a-button type="outline" status="danger" @click="removeParam(param.id)">
                 <icon-delete />
               </a-button>
@@ -201,15 +207,16 @@ const handleParamChange = () => {
 
 // 添加参数
 const addParam = () => {
-  api_params.value = [...api_params.value, { id: `${Date.now()+Math.floor(Math.random() * 100)}`, key: '', value: '' }];
+  // api_params.value = [...api_params.value, { id: `${Date.now() + Math.floor(Math.random() * 100)}`, key: '', value: '', default_value: '' }];
+  api_params.value = [...api_params.value, { id: `${Date.now() + Math.floor(Math.random() * 100)}`, key: '', value: '' }];
   notifyParent();
 };
 
 // 删除参数
 const removeParam = (id) => {
-  console.log(id,999)
+  // console.log(id,999)
   api_params.value = api_params.value.filter((param) => param.id !== id);
-  console.log(api_params.value,888)
+  // console.log(api_params.value,888)
   // api_params.value = api_params.value.filter((param) => param.id !== id);
   notifyParent();
 };

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

@@ -120,6 +120,8 @@
                 <a-option value="limit_to_yesterday_end"> 昨天的 23:59:59 </a-option>
                 <a-option value="limit_to_day_start"> 今天的 00:00:00 </a-option>
                 <a-option value="limit_to_day_end"> 今天的 23:59:59 </a-option>
+                <a-option value="limit_to_daybeforeyesterday_start"> 前天的 00:00:00 </a-option>
+                <a-option value="limit_to_daybeforeyesterday_end"> 前天的 23:59:59 </a-option>
               </a-select>
             </td>
             <td>

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

@@ -100,6 +100,8 @@
                 <a-option value="limit_to_yesterday_end"> 昨天的 23:59:59 </a-option>
                 <a-option value="limit_to_day_start"> 今天的 00:00:00 </a-option>
                 <a-option value="limit_to_day_end"> 今天的 23:59:59 </a-option>
+                <a-option value="limit_to_daybeforeyesterday_start"> 前天的 00:00:00 </a-option>
+                <a-option value="limit_to_daybeforeyesterday_end"> 前天的 23:59:59 </a-option>
               </a-select>
             </td>
             <td>

+ 25 - 25
src/modules/data-source-manage/data-source/DataSourceBody.vue

@@ -223,30 +223,30 @@ const handleCancel = () => {
   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 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;
@@ -291,7 +291,7 @@ const handleEdit = (record) => {
 
 const handleCopy = (record) => {
   show_copy_modal.value = true;
-  console.log(record);
+  // console.log(record);
   target_copy_id.value = record.id;
   // default_name.value = record.metric_name;
 };

+ 23 - 9
src/modules/report-template/TemplateDetail.vue

@@ -25,9 +25,10 @@
         @update-draft="handleUpdateDraft"
         @send-report="handleSendReport"
       ></EditorModel>
+      <!-- v-if="show_filter_model" -->
       <FilterModel
-        v-if="show_filter_model"
         ref="filterModelRef"
+        v-if="show_filter_model"
         :filter_type="target_filter_type"
         :filter_option="target_filter_option"
         :target_common_config_info="target_common_config_info"
@@ -95,7 +96,7 @@ const props = defineProps({
 
 const emit = defineEmits(['save-template', 'update-draft', 'send-report']);
 
-const show_filter_model = ref(false);
+const show_filter_model = ref(true);
 const target_filter_option = ref(null);
 const target_filter_type = ref('mention');
 const is_metrics_loading = ref(false); // 指标列表加载中
@@ -145,7 +146,7 @@ const handleMentionClick = (option) => {
   });
 };
 const handleChartClick = (option) => {
-  console.log(option, 890);
+  // console.log(option, 890);
   target_source_id.value = option.chartId || '';
   // console.log(target_source_id.value);
   target_filter_option.value = null;
@@ -164,27 +165,35 @@ const handleMetricsClick = (item) => {
     target_filter_option.value.option.metric_name === item.metric_name
   ) {
   } else {
-    show_filter_model.value = false;
+    // show_filter_model.value = false;
+    // closeFilterModel();
   }
 };
 
 const closeFilterModel = () => {
+  // target_filter_type.value = 'mention';
+  target_filter_option.value = null;
+  // target_common_config_info.value = [];
   show_filter_model.value = false;
+  nextTick(() => {
+    show_filter_model.value = true;
+  });
 };
 
 const getMetricsFun = async () => {
   try {
     is_metrics_loading.value = true;
     const res = await getMetrics({ metric_name: '', page: 1, size: 100000 });
-    console.log(res);
+    // console.log(res);
     if (res.code / 1 === 200) {
       data_source_list.value = res.data;
+      reportEditor.updateSourceList(res.data);
       target_source_id.value = data_source_list.value[0]?.id;
       // console.log(target_source_id.value, 890);
       if (data_source_list.value.length > 0) {
         const flexible_template_metric_fields = data_source_list.value[0].flexible_template_metric_fields;
         if (flexible_template_metric_fields.length) {
-          console.log(data_source_list.value);
+          // console.log(data_source_list.value);
           setDataToMetricsAndDimension(flexible_template_metric_fields, data_source_list.value[0]);
           getTargetCommonConfigInfo(flexible_template_metric_fields);
         }
@@ -250,12 +259,17 @@ const handleChangeSource = (id) => {
       getTargetCommonConfigInfo(item.flexible_template_metric_fields);
     }
   });
-  show_filter_model.value = false;
+  // show_filter_model.value = false;
+  closeFilterModel();
+  // show_filter_model.value = false;
+  // nextTick(() => {
+  //   show_filter_model.value = true;
+  // });
 };
 const handleSubmitHtml = (html) => {
   submit_html.value = html;
   const md = formatHtmlToMd(submit_html.value);
-  console.log(md);
+  // console.log(md);
   // 检查是否包含非ASCII字符
   const hasNonAscii = /[^\x00-\x7F]/.test(md);
   // console.log('包含非ASCII字符:', hasNonAscii);
@@ -271,7 +285,7 @@ const handleSaveTemplate = (data) => {
 };
 
 const handleContentChanged = funcDebounce(() => {
-  console.log('需要保存');
+  // console.log('需要保存');
   if (props.template_id) {
     reportEditor.setIsNeedSaveTemplate(true);
   } else {

+ 34 - 2
src/modules/report-template/TemplateEditor.vue

@@ -62,6 +62,8 @@ const props = defineProps({
 
 const template_content = ref(props.target_outline_md||'')
 const template_config = ref([])
+const used_source_list = ref([])
+
 const common_config = ref({})
 
 const template_name = ref('')
@@ -88,11 +90,21 @@ const getTemplateDetail = async () => {
     try{
       const res = await checkTemplateDetail({id: props.target_template_id})
       if(res.code/1 === 200) {
-        console.log(res,'模板详情')
         const content = res.data.content
         template_content.value = content
+        if(res.data.source_list){
+          used_source_list.value = JSON.parse(res.data.source_list)
+          reportEditor.updateUsedSourceList(used_source_list.value)
+        }
         if(res.data.template_config){
           template_config.value = JSON.parse(res.data.template_config)
+          template_config.value.forEach(item => {
+            const it = used_source_list.value.find(source => source.id === item.option.metric_id)
+            if (it) {
+              item.option.source = it
+            }
+          })
+          console.log(template_config.value, 9090)
         }
         if(res.data.common_config){
          common_config.value = JSON.parse(res.data.common_config)
@@ -174,11 +186,31 @@ const toNotSaveSetting = () => {
 const editTemplateFunc = async (id) => {
   try {
     is_loading_edit.value = true
+    const source_list = reportEditor.metrics_list.map((item) => item.option?.source);
+    const uniqueMap = new Map();
+    source_list.forEach((item) => {
+      // 只有当 Map 中不存在该 id 时才存入,从而保留第一项
+      if (!uniqueMap.has(item.id)) {
+        uniqueMap.set(item.id, item);
+      }
+    });
+    const used_source_list = Array.from(uniqueMap.values());
+    const template_config = reportEditor.metrics_list.map((item) => {
+      const { source, ...otherOptionProps } = item.option;
+      return {
+        ...item,
+        option: otherOptionProps
+      };
+    });
+    console.log(template_config, 'template_config');
+    console.log(used_source_list, 'used_source_list');
     const res = await editTemplateItem({
       id,
       name: template_name.value,
       content: reportEditor.target_report_markdown,
-      template_config: JSON.stringify(reportEditor.metrics_list),
+      // template_config: JSON.stringify(reportEditor.metrics_list),
+      template_config: JSON.stringify(template_config),
+      source_list: JSON.stringify(used_source_list),
       common_config: JSON.stringify(common_config.value)
     });
     if (res.code / 1 === 200) {

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

@@ -77,6 +77,9 @@
         <div>
           <button @click="insertData">插入数据</button>
         </div> -->
+        <div class="special_btn">
+          <span class="btn" title="<special>" @click="insertSpecialTag">&lt;special&gt;</span>
+        </div>
       </div>
     </div>
     <div class="common-config">
@@ -876,6 +879,129 @@ function setSelectionToRange(range) {
   sel.addRange(range);
 }
 
+// function insertSpecialTag() {
+//   const sel = window.getSelection();
+//   if (!editorRef.value) return;
+
+//   const insertRange = normalizeInsertRange();
+//   setSelectionToRange(insertRange);
+//   if (!sel?.rangeCount) return;
+
+//   const range = sel.getRangeAt(0);
+//   const specialContent = document.createElement('span');
+//   specialContent.setAttribute('data-special', 'true');
+//   specialContent.setAttribute('data-placeholder', '请输入特别提示词');
+//   specialContent.contentEditable = 'true';
+//   specialContent.className = 'special-placeholder';
+//   specialContent.style.cssText =
+//     'background-color: #fff3cd; border: 1px solid #ffeeba; border-radius: 4px; padding: 2px 8px; color: #856404; font-weight: 500; min-width: 200px; display: inline-block; outline: none; font-size: inherit; font-family: inherit;';
+
+//   // 关键:放一个零宽度的隐形字符,这样点击 span 任何地方,光标都能进去
+//   specialContent.innerText = '\u200B请输入特别提示词,以此统一文本结构、规范输出样式';
+
+//   range.deleteContents();
+//   range.insertNode(specialContent);
+//   range.insertNode('\u200B');
+//   // 添加 focus 事件监听:光标移入时如果内容是提示文字则清空
+//   specialContent.addEventListener('focus', function (e) {
+//     e.stopPropagation();
+//     const placeholder = this.getAttribute('data-placeholder');
+//     console.log('focus event triggered:', placeholder, this.innerText);
+//     if (this.innerText.includes(placeholder)) {
+//       this.innerText = '\u200B';
+//     }
+//   });
+
+//   immediateSave();
+//   emit('submitHtml', editorRef.value.innerHTML);
+//   emit('content-changed', editorRef.value.innerHTML);
+// }
+function insertSpecialTag() {
+  const sel = window.getSelection();
+  if (!editorRef.value) return;
+
+  // ----------------【新增:防嵌套限制条件】----------------
+  // 检查当前光标聚焦的节点(sel.anchorNode)是否已经在我们自定义的 input 内部
+  // 如果是在 input 内部,或者选区的父节点是特殊 input,直接拦截不执行
+  if (document.activeElement?.tagName === 'INPUT' && document.activeElement.getAttribute('data-special') === 'true') {
+    console.warn('当前光标已在特殊输入框内,不允许重复创建!');
+    return; // 直接退出函数
+  }
+  // ------------------------------------------------------
+
+  const insertRange = normalizeInsertRange();
+  setSelectionToRange(insertRange);
+  if (!sel?.rangeCount) return;
+
+  const range = sel.getRangeAt(0);
+  const placeholderText = '请输入特别提示词,以此统一文本结构、规范输出样式';
+
+  // 1. 创建真正的 input 元素
+  const inputEl = document.createElement('input');
+  inputEl.type = 'text';
+  inputEl.setAttribute('data-special', 'true');
+  inputEl.placeholder = placeholderText;
+  inputEl.contentEditable = 'true'; // 防止 input 自身被当成富文本修改
+
+  // 2. 赋予样式
+  inputEl.style.cssText = `
+    background-color: #fff3cd;
+    border: 1px solid #ffeeba;
+    border-radius: 4px;
+    padding: 2px 8px;
+    color: #856404;
+    font-weight: 500;
+    min-width: 360px;
+    display: inline-block;
+    outline: none;
+    font-size: inherit;
+    font-family: inherit;
+    vertical-align: middle;
+    box-sizing: border-box;
+    margin-left:5px;
+  `;
+
+  // 3. 动态控制宽度
+  function handleInputWidth(el) {
+    const valLength = el.value.length;
+    if (valLength > 10) {
+      el.style.width = valLength * 14 + 20 + 'px';
+    } else {
+      el.style.width = '200px';
+    }
+  }
+
+  // 4. 事件绑定
+  function bindInputEvents(el) {
+    el.oninput = function () {
+      handleInputWidth(this);
+      this.setAttribute('value', this.value); // 确保 innerHTML 里带有最新值,方便转 MD 拦截
+      emit('content-changed', editorRef.value.innerHTML);
+    };
+  }
+
+  bindInputEvents(inputEl);
+  handleInputWidth(inputEl);
+
+  // 5. 插入节点
+  range.deleteContents();
+  range.insertNode(inputEl);
+
+  // 6. 数据保存与数据派发
+  immediateSave();
+  emit('submitHtml', editorRef.value.innerHTML);
+  emit('content-changed', editorRef.value.innerHTML);
+
+  // 7. 延迟聚焦
+  setTimeout(() => {
+    const allInputs = editorRef.value.querySelectorAll('input[data-special="true"]');
+    const targetInput = allInputs[allInputs.length - 1];
+    if (targetInput) {
+      bindInputEvents(targetInput);
+      targetInput.focus();
+    }
+  }, 10);
+}
 function insertOption(opt, options = {}) {
   const sel = window.getSelection();
   if (!editorRef.value) return null;
@@ -1193,10 +1319,41 @@ function hideActionMenu() {
   showActionMenu.value = false;
   currentMentionElement.value = null;
 }
+
+function regenerateMetricUid(oldUid) {
+  if (!oldUid) return null;
+
+  const newUid = generateUID();
+
+  const mentionElement = editorRef.value.querySelector(`[data-uid="${oldUid}"]`);
+  if (mentionElement) {
+    mentionElement.setAttribute('data-uid', newUid);
+  }
+
+  const mentionDataIndex = mentionsData.value.findIndex((item) => item.uid === oldUid);
+  if (mentionDataIndex !== -1) {
+    mentionsData.value[mentionDataIndex].uid = newUid;
+  }
+
+  const metricItem = reportEditor.metrics_list.find((item) => item.index === oldUid);
+  if (metricItem) {
+    metricItem.uid = newUid;
+    metricItem.index = newUid;
+  }
+
+  // 通知父组件富文本内容已修改
+  if (editorRef.value) {
+    emit('submitHtml', editorRef.value.innerHTML);
+    emit('content-changed', editorRef.value.innerHTML);
+  }
+
+  return newUid;
+}
+
 // 编辑器中的点击事件合集
 function handleEditorClick(e) {
-  console.log('handleEditorClick', e.target);
-  console.log('handleEditorClick', actionMenuRef.value.$el);
+  // 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();
@@ -1231,7 +1388,11 @@ function handleEditorClick(e) {
   // 如果点击的是提及
   const mention = e.target.closest('[data-mention]');
   if (mention) {
-    const uid = mention?.getAttribute('data-uid');
+    const oldUid = mention?.getAttribute('data-uid');
+    const newUid = regenerateMetricUid(oldUid);
+    const uid = newUid || oldUid;
+    // const uid = oldUid;
+
     const option = mentionsData.value.find((item) => item.uid === uid);
     reportEditor.setTargetMetricId(uid);
 
@@ -1239,11 +1400,10 @@ function handleEditorClick(e) {
     mention.classList.add('mention-highlighted');
     // console.log(option.option.advanced_computing_config);
     // console.log(JSON.parse(option.option.advanced_computing_config));
-    console.log(option.option, 999);
+    // console.log(option.option, 999);
     target_action.value = JSON.parse(option.option.source.advanced_computing_config);
     // 保存当前指标引用,用于后续位置调整
     currentMentionElement.value = mention;
-
     // 给父组件一个触发一个事件,显示过滤器
     emit('mention-click', option);
 
@@ -1882,6 +2042,50 @@ function calculateChartPanelPosition() {
     left: buttonRect.left - containerRect.left
   };
 }
+
+// 检查指标是否需要标红(存在 is_extend 为 0 的项)
+function checkMetricNeedRed(metric) {
+  if (!metric || !metric.extend_commom_config_info) return false;
+  return metric.extend_commom_config_info.some((item) => item && item.is_extend / 1 === 0);
+}
+
+// 更新指标的红色警告样式
+function updateMetricRedWarning() {
+  if (!editorRef.value) return;
+
+  const allMentions = editorRef.value.querySelectorAll('[data-mention][data-uid]');
+  // console.log(99999, allMentions);
+  allMentions.forEach((mention) => {
+    const uid = mention.getAttribute('data-uid');
+    const metric = reportEditor.metrics_list.find((item) => item && item.uid === uid);
+    // console.log(uid, metric);
+    const needRed = checkMetricNeedRed(metric);
+
+    if (needRed) {
+      mention.classList.add('mention-red-warning');
+    } else {
+      mention.classList.remove('mention-red-warning');
+    }
+  });
+}
+
+// 监听 metrics_list 变化,实时更新红色警告样式
+watch(
+  () => reportEditor.metrics_list,
+  () => {
+    nextTick(() => {
+      updateMetricRedWarning();
+    });
+  },
+  { deep: true }
+);
+
+// 组件挂载时初始化样式
+onMounted(() => {
+  nextTick(() => {
+    updateMetricRedWarning();
+  });
+});
 </script>
 
 <style scoped>
@@ -1916,6 +2120,16 @@ function calculateChartPanelPosition() {
   align-items: center;
   gap: 8px;
   padding: 8px;
+  padding-right: 20px;
+}
+.special_btn .btn {
+  text-decoration: none;
+  color: var(--primary-default);
+  font-weight: 500;
+  cursor: pointer;
+}
+.special_btn .btn:hover {
+  opacity: 0.8;
 }
 
 .toolbar .btn-icon {
@@ -2030,6 +2244,18 @@ function calculateChartPanelPosition() {
   box-shadow: 0 0 0 2px var(--primary-light-2);
 }
 
+/* 指标红色警告样式(当 extend_commom_config_info 中存在 is_extend 为 0 时) */
+:deep(.editor [data-mention].mention-red-warning) {
+  background-color: #fff2f0 !important;
+  color: #ff4d4f !important;
+  border: 1px solid #ffccc7 !important;
+}
+
+/* 指标红色警告时,内部 SVG 图标也标红 */
+:deep(.editor [data-mention].mention-red-warning svg path) {
+  fill: #ff4d4f !important;
+}
+
 /* 图表选中样式 */
 :deep(.editor [data-chart].chart-selected) {
   border: 2px solid var(--primary-default) !important;
@@ -2074,4 +2300,44 @@ function calculateChartPanelPosition() {
 .mention-action-menu button:hover {
   background-color: #f0f0f0;
 } */
+
+/* special 标签的 placeholder 效果 */
+:deep(.editor [data-special].special-placeholder:empty::before) {
+  content: attr(data-placeholder);
+  color: #aaa;
+  pointer-events: none;
+}
+</style>
+<style>
+.special-placeholder {
+  background-color: #fff3cd;
+  border: 1px solid #ffeeba;
+  border-radius: 4px;
+  padding: 2px 8px;
+  color: #856404;
+  font-weight: 500;
+  min-width: 200px;
+
+  /* 关键修改:用 inline-flex 代替 inline-block,能让光标更好地在空内容时定位 */
+  display: inline-block;
+  align-items: center;
+  min-height: 24px; /* 给一个保底高度 */
+
+  outline: none;
+  font-size: inherit;
+  font-family: inherit;
+  vertical-align: middle;
+}
+
+/* 当为空时显示占位符 */
+.special-placeholder.is-empty::before {
+  content: attr(data-placeholder);
+  color: #856404;
+  opacity: 0.6;
+  font-weight: normal;
+  pointer-events: none;
+
+  /* 关键修改:去掉 absolute,让它正常撑开内部,但如果没有内容时它就是唯一的展示物 */
+  white-space: nowrap;
+}
 </style>

+ 42 - 6
src/modules/report-template/editor-model/EditorTitleHeader.vue

@@ -257,15 +257,51 @@ const handleSendReport = async () => {
 const editTemplateFunc = async (id, type = 1) => {
   try {
     is_loading_template.value = true;
+    const source_list = reportEditor.metrics_list.map((item) => item.option?.source);
+    const uniqueMap = new Map();
+    source_list.forEach((item) => {
+      // 只有当 Map 中不存在该 id 时才存入,从而保留第一项
+      if (!uniqueMap.has(item.id)) {
+        uniqueMap.set(item.id, item);
+      }
+    });
+    const used_source_list = Array.from(uniqueMap.values());
+    const template_config = reportEditor.metrics_list.map((item) => {
+      const { source, ...otherOptionProps } = item.option;
+      return {
+        ...item,
+        option: otherOptionProps
+      };
+    });
+    console.log(template_config, 'template_config');
+    console.log(used_source_list, 'used_source_list');
+    // return;
+    console.log(
+      {
+        id,
+        name: title.value,
+        content: reportEditor.target_report_markdown,
+        // template_config: JSON.stringify(reportEditor.metrics_list),
+        template_config: template_config,
+        source_list: used_source_list,
+        common_config: reportEditor.common_config,
+        type: type
+      },
+      'editTemplateItem'
+    );
+    // return;
+
     const res = await editTemplateItem({
       id,
       name: title.value,
       content: reportEditor.target_report_markdown,
-      template_config: JSON.stringify(reportEditor.metrics_list),
+      // template_config: JSON.stringify(reportEditor.metrics_list),
+      template_config: JSON.stringify(template_config),
+      source_list: JSON.stringify(used_source_list),
       common_config: JSON.stringify(reportEditor.common_config),
       type: type
     });
-    console.log(212, reportEditor.metrics_list);
+    // console.log(212, reportEditor.metrics_list);
     if (res.code / 1 === 200) {
       return res;
     } else {
@@ -291,7 +327,7 @@ const addTemplateFunc = async (type = 1) => {
       type: type
     });
     if (res.code / 1 === 200) {
-      console.log(res);
+      // console.log(res);
       // Message.success('保存成功');
       return res;
     } else {
@@ -343,8 +379,8 @@ watch(
 watch(
   () => is_need_save.value,
   async (newVal, oldVal) => {
-    console.log(newVal, props.template_id, props.draft_id, 99999999999999);
-    console.log(props.template_id && props.draft_id);
+    // console.log(newVal, props.template_id, props.draft_id, 99999999999999);
+    // console.log(props.template_id && props.draft_id);
     if (newVal) {
       if (!props.template_id && !props.draft_id) {
         const res = await addTemplateFunc(2);
@@ -362,7 +398,7 @@ watch(
         }
       }
       if (!props.template_id && props.draft_id) {
-        console.log(props.draft_id, 99999999999999);
+        // console.log(props.draft_id, 99999999999999);
         const res = await editTemplateFunc(props.draft_id, 2);
         if (res.code / 1 === 200) {
           emit('update-draft', {

+ 29 - 4
src/modules/report-template/filter-model/DimensionItem.vue

@@ -5,6 +5,11 @@
     <icon-delete class="item-end-icon" @click.stop="handleClick(dimension_item)" />
   </div>
   <DeleteModal :visible="show_delete_modal" @ok="handleConfirm" @cancel="handleCancel"></DeleteModal>
+  <DeleteDimGroupModal
+    :visible="show_delete_group_dim_model"
+    @ok="handleDimGroupConfirm"
+    @cancel="handleDimGroupCancel"
+  ></DeleteDimGroupModal>
 </template>
 
 <script setup >
@@ -12,12 +17,17 @@ import { ref } from 'vue';
 import DimensionDate from '../../../assets/report-template/diemension_date.svg';
 import DimensionStr from '../../../assets/report-template/diemension_str.svg';
 import DeleteModal from '../../../components/DeleteModal.vue';
+import DeleteDimGroupModal from '../../../components/DeleteDimGroupModal .vue';
+import { useReportEditor } from '../../../base/store/use-report-editor';
+const reportEditor = useReportEditor();
 const props = defineProps({
-  dimension_item: Object
+  dimension_item: Object,
+  group_list: Array
 });
-const emit = defineEmits(['remove', 'click']);
+const emit = defineEmits(['remove', 'remove_by_dim_group', 'click']);
 
 const show_delete_modal = ref(false);
+const show_delete_group_dim_model = ref(false);
 const target_delete_item = ref(null);
 const getIconByType = (type) => {
   if (type === 'str') return DimensionStr;
@@ -27,8 +37,14 @@ const getIconByType = (type) => {
 
 const handleClick = (item) => {
   // emit('remove', item.id);
-  target_delete_item.value = item;
-  show_delete_modal.value = true;
+  const idx = props.group_list.findIndex((group) => group.field_key === item.field_key);
+  if (idx !== -1) {
+    show_delete_group_dim_model.value = true;
+    target_delete_item.value = item;
+  } else {
+    target_delete_item.value = item;
+    show_delete_modal.value = true;
+  }
 };
 
 const handleItemClick = () => {
@@ -46,6 +62,15 @@ const handleCancel = () => {
 const clearTargetDeleteItem = () => {
   target_delete_item.value = null;
 };
+const handleDimGroupConfirm = () => {
+  emit('remove_by_dim_group', target_delete_item.value);
+  show_delete_group_dim_model.value = false;
+  clearTargetDeleteItem();
+};
+const handleDimGroupCancel = () => {
+  show_delete_group_dim_model.value = false;
+  clearTargetDeleteItem();
+};
 </script>
 <style scoped lang="css">
 .dimension-item {

+ 241 - 0
src/modules/report-template/filter-model/GroupItem.vue

@@ -0,0 +1,241 @@
+<template>
+  <div class="group-item-wrapper">
+    <div class="group-item-inner" @click="handleItemClick" ref="itemRef">
+      <img class="icon" :src="getIconByType(group_item.field_type)" alt="" />
+      <span class="field-name">{{ group_item.field_name }}</span>
+      <span class="top-value" v-if="group_item.top_n">Top{{ group_item.top_n }}</span>
+      <icon-caret-down class="arrow-icon" />
+      <icon-delete class="delete-icon" @click.stop="handleClick(group_item)" />
+    </div>
+  </div>
+
+  <!-- 使用 Teleport 将弹出框渲染到 body -->
+  <Teleport to="body">
+    <div class="top-popup" v-if="show_top_popup" @click.stop :style="popupStyle">
+      <div class="popup-option" @click="selectTop(3)">Top3</div>
+      <div class="popup-option" @click="selectTop(5)">Top5</div>
+      <div class="popup-option" @click="selectTop(10)">Top10</div>
+      <div class="popup-divider"></div>
+      <div class="custom-input">
+        <input
+          type="number"
+          v-model="custom_top_value"
+          placeholder="自定义"
+          @keyup.enter="confirmCustomTop"
+          ref="customInputRef"
+        />
+        <button class="confirm-btn" @click="confirmCustomTop">确定</button>
+      </div>
+    </div>
+  </Teleport>
+
+  <DeleteModal :visible="show_delete_modal" @ok="handleConfirm" @cancel="handleCancel"></DeleteModal>
+</template>
+<script setup >
+import { ref, nextTick, computed, onMounted, onUnmounted } from 'vue';
+import DimensionDate from '../../../assets/report-template/diemension_date.svg';
+import DimensionStr from '../../../assets/report-template/diemension_str.svg';
+import DeleteModal from '../../../components/DeleteModal.vue';
+const props = defineProps({
+  group_item: Object
+});
+const emit = defineEmits(['remove', 'click', 'update-top-n']);
+
+const show_delete_modal = ref(false);
+const show_top_popup = ref(false);
+const target_delete_item = ref(null);
+const custom_top_value = ref('');
+const customInputRef = ref(null);
+const itemRef = ref(null);
+const popupPosition = ref({ top: 0, left: 0 });
+
+const popupStyle = computed(() => ({
+  position: 'fixed',
+  top: `${popupPosition.value.top}px`,
+  left: `${popupPosition.value.left}px`
+}));
+
+const handleClickOutside = (event) => {
+  if (show_top_popup.value && itemRef.value && !itemRef.value.contains(event.target)) {
+    show_top_popup.value = false;
+    custom_top_value.value = '';
+  }
+};
+
+onMounted(() => {
+  document.addEventListener('click', handleClickOutside);
+});
+
+onUnmounted(() => {
+  document.removeEventListener('click', handleClickOutside);
+});
+
+const getIconByType = (type) => {
+  if (type === 'str') return DimensionStr;
+  if (type === 'int') return DimensionStr || '';
+  if (type === 'date') return DimensionDate;
+};
+
+const handleClick = (item) => {
+  target_delete_item.value = item;
+  show_delete_modal.value = true;
+};
+
+const handleItemClick = () => {
+  show_top_popup.value = !show_top_popup.value;
+  if (show_top_popup.value) {
+    nextTick(() => {
+      calculatePopupPosition();
+      customInputRef.value?.focus();
+    });
+  }
+};
+
+const calculatePopupPosition = () => {
+  if (itemRef.value) {
+    const rect = itemRef.value.getBoundingClientRect();
+    const popupHeight = 140;
+    const gap = 8;
+    popupPosition.value = {
+      top: rect.top - popupHeight - gap,
+      left: rect.left
+    };
+  }
+};
+
+const selectTop = (value) => {
+  emit('update-top-n', { group_id: props.group_item.group_id, top_n: value });
+  show_top_popup.value = false;
+  custom_top_value.value = '';
+};
+
+const confirmCustomTop = () => {
+  const value = parseInt(custom_top_value.value);
+  if (value && value > 0) {
+    console.log(value, 99999);
+    emit('update-top-n', { group_id: props.group_item.group_id, top_n: value });
+    show_top_popup.value = false;
+    custom_top_value.value = '';
+    return;
+  }
+  if (custom_top_value.value === '') {
+    emit('update-top-n', { group_id: props.group_item.group_id, top_n: custom_top_value.value });
+    show_top_popup.value = false;
+    custom_top_value.value = '';
+  }
+};
+
+const handleConfirm = () => {
+  emit('remove', target_delete_item.value);
+  show_delete_modal.value = false;
+  clearTargetDeleteItem();
+};
+const handleCancel = () => {
+  show_delete_modal.value = false;
+  clearTargetDeleteItem();
+};
+const clearTargetDeleteItem = () => {
+  target_delete_item.value = null;
+};
+</script>
+<style scoped lang="css">
+.group-item-wrapper {
+  position: relative;
+  display: block;
+}
+.group-item-inner {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  background: #e0eaff;
+  color: #181d1d;
+  padding: 6px 12px;
+  border: 1px solid #275fd4;
+  border-radius: 6px;
+  cursor: pointer;
+}
+.icon {
+  margin-top: 2px;
+  flex-shrink: 0;
+}
+.field-name {
+  flex: 1;
+  min-width: 0;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.top-value {
+  font-size: 12px;
+  color: #275fd4;
+  font-weight: 500;
+  flex-shrink: 0;
+}
+.arrow-icon {
+  color: #275fd4;
+  flex-shrink: 0;
+  font-size: 14px;
+}
+.delete-icon {
+  color: #275fd4;
+  flex-shrink: 0;
+  font-size: 14px;
+  display: none;
+}
+.group-item-inner:hover .delete-icon {
+  display: inline-block;
+}
+
+/* Top弹出框样式 - 使用 fixed 定位 */
+.top-popup {
+  background: #ffffff;
+  border-radius: 8px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  padding: 4px 0;
+  min-width: 120px;
+  z-index: 9999;
+  border: 1px solid #e8e8e8;
+}
+.popup-option {
+  padding: 8px 16px;
+  cursor: pointer;
+  font-size: 14px;
+  color: #333333;
+}
+.popup-option:hover {
+  background: #f0f5ff;
+}
+.popup-divider {
+  height: 1px;
+  background: #e8e8e8;
+  margin: 4px 0;
+}
+.custom-input {
+  display: flex;
+  padding: 4px 8px;
+  gap: 4px;
+}
+.custom-input input {
+  flex: 1;
+  padding: 4px 8px;
+  border: 1px solid #d9d9d9;
+  border-radius: 4px;
+  font-size: 12px;
+  outline: none;
+}
+.custom-input input:focus {
+  border-color: var(--primary-default);
+}
+.confirm-btn {
+  padding: 4px 12px;
+  background: var(--primary-default);
+  color: #ffffff;
+  border: none;
+  border-radius: 4px;
+  font-size: 12px;
+  cursor: pointer;
+}
+.confirm-btn:hover {
+  background: var(--primary-default);
+}
+</style>

+ 326 - 47
src/modules/report-template/filter-model/MentionFilter.vue

@@ -8,14 +8,15 @@
         <div class="title"><span style="color: red">*</span>度量</div>
         <div class="measure-list">
           <a-popover>
-            <div class="measure-item">
+            <div class="measure-item" v-if="measure_name">
               <img :src="MetricsIcon" alt="" />
-                <span class="measure-name">{{ measure_name }}</span>
-                <span v-if="measure_aggregate" class="measure-aggregate">({{ measure_aggregate }})</span>
+              <span class="measure-name">{{ measure_name }}</span>
+              <span v-if="measure_aggregate" class="measure-aggregate">({{ measure_aggregate }})</span>
               <!-- <icon-caret-down class="item-end-icon" /> -->
             </div>
+            <div v-else class="empty-tip">暂无度量</div>
             <template #content>
-              <p style="font-size: 14px; color: #666">度量名称:{{ measure_name }}</p>
+              <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>
@@ -29,15 +30,23 @@
             v-for="item in dimension_list"
             :key="item.dimesion_id"
             :dimension_item="item"
+            :group_list="group_list"
             @remove="handleDimensionRemove(item.dimesion_id)"
+            @remove_by_dim_group="handleDimensionRemoveByDimGroup(item.dimesion_id)"
           ></DimensionItem>
-          <div v-if="!dimension_list.length" class="empty-tip">拖动字段到此处</div>
+          <div v-if="!dimension_list.length" class="empty-tip">拖动字段到此处</div>
           <!-- <div class="filter-item-tip">拖动数字段到此处</div> -->
           <!-- <a-empty v-if="!dimension_list.length"></a-empty> -->
         </div>
       </div>
       <div class="filter" @dragover="onDragOver" @drop="onDropFilter">
-        <div class="title">过滤器</div>
+        <div class="title">
+          <span>过滤器</span>
+          <div class="tools" v-if="false">
+            <img class="icon" :src="CopyIcon" alt="复制" title="复制" />
+            <img class="icon" :src="PasteIcon" alt="粘贴" title="粘贴" />
+          </div>
+        </div>
         <div class="filter-list">
           <DimensionItem
             class="dimension-item"
@@ -47,7 +56,7 @@
             @click="handleFilterClick(item)"
             @remove="handleFilterRemove(item.filter_id)"
           ></DimensionItem>
-          <div v-if="!filter_list.length" class="empty-tip">拖动字段到此处</div>
+          <div v-if="!filter_list.length" class="empty-tip">拖动字段到此处</div>
           <!-- <div class="filter-item-tip">拖动数字段到此处</div> -->
           <!-- <div class=""></div> -->
         </div>
@@ -55,13 +64,46 @@
       <div class="filter-config">
         <div class="title">全局取消继承</div>
         <div class="filter-config-body">
-          <div class="filter-config-body-item" v-for="item in common_config_list" :key="item.field_key">
-            <a-checkbox v-model="item.is_checked" @change="(event) => handleCommonConfigClick(item, event)">{{
-              item.field_name
-            }}</a-checkbox>
-          </div>
+          <template v-if="props.filter_option">
+            <div class="filter-config-body-item" v-for="item in common_config_list" :key="item.field_key">
+              <a-checkbox v-model="item.is_checked" @change="(event) => handleCommonConfigClick(item, event)">{{
+                item.field_name
+              }}</a-checkbox>
+            </div>
+          </template>
+          <a-empty v-if="!common_config_list.length"></a-empty>
         </div>
       </div>
+      <div class="group">
+        <div class="title">分组</div>
+        <div class="group-list" @dragover="onDragOver" @drop="onDropGroup">
+          <GroupItem
+            class="dimension-item"
+            v-for="item in group_list"
+            :key="item.group_id"
+            :group_item="item"
+            @remove="handleGroupRemove(item.group_id)"
+            @update-top-n="handleUpdateTopN"
+          ></GroupItem>
+          <div v-if="!group_list.length" class="empty-tip">拖动字段到此处</div>
+          <!-- <div class="filter-item-tip">拖动数字段到此处</div> -->
+          <!-- <a-empty v-if="!dimension_list.length"></a-empty> -->
+        </div>
+      </div>
+      <!-- <div class="filter-time"> -->
+      <!-- <span>动态时间</span> -->
+      <!-- <a-switch v-model="dynamic_time" type="round" checked-value="开启" unchecked-value="关闭" /> -->
+      <!-- <a-switch
+          v-model="dynamic_time"
+          type="round"
+          :checked-value="1"
+          :unchecked-value="0"
+          checked-color="var(--primary-default)"
+          checked-text="启用"
+          unchecked-text="停用"
+          @change="handleStatusChange(record)"
+        ></a-switch> -->
+      <!-- </div> -->
     </div>
   </div>
   <StringFilter
@@ -91,16 +133,26 @@
     @confirm="handleNumberConfirm"
     @cancel="closeNumberModel"
   ></NumberFilter>
+  <GroupReplaceModal
+    v-if="show_tips_by_group"
+    :visible="show_tips_by_group"
+    @ok="handleGroupReplaceConfirm"
+    @cancel="closeGroupReplaceModal"
+  ></GroupReplaceModal>
 </template>
 
 <script setup>
 import { ref, watch, computed, onMounted, onUnmounted } from 'vue';
 import { Message } from '@arco-design/web-vue';
+import CopyIcon from '../../../assets/chat-report/copy.svg';
+import PasteIcon from '../../../assets/chat-report/paste.svg';
 import MetricsIcon from '../../../assets/report-template/metrics_icon.svg';
 import DimensionItem from './DimensionItem.vue';
+import GroupItem from './GroupItem.vue';
 import StringFilter from './model/StringModel.vue';
 import DateFilter from './model/DateModel.vue';
 import NumberFilter from './model/NumberModel.vue';
+import GroupReplaceModal from '../../../components/GroupReplaceModal.vue';
 import { generateID } from '../editor-model/common-tools';
 import { useReportEditor } from '../../../base/store/use-report-editor';
 import { num_form_default_data, str_form_default_data, date_form_default_data } from './filter_data';
@@ -120,6 +172,8 @@ const props = defineProps({
 
 const emit = defineEmits(['content-changed']);
 
+const dynamic_time = ref(false);
+
 const aggregation_map = {
   sum: '求和',
   avg: '求平均值',
@@ -129,10 +183,13 @@ const aggregation_map = {
 };
 
 const filter_title = computed(() => {
-  return '指标 — ' + props.filter_option.option.field_name;
+  if (!props.filter_option?.option?.field_name) {
+    return '指标';
+  }
+  return '指标 — ' + props.filter_option?.option?.field_name || '';
 });
 const measure_name = computed(() => {
-  return props.filter_option.option.field_name;
+  return props.filter_option?.option?.field_name || '';
 });
 const measure_aggregate = computed(() => {
   return aggregation_map[reportEditor.QueryTargetAggregationType] || '无聚合';
@@ -140,6 +197,10 @@ const measure_aggregate = computed(() => {
 
 const dimension_list = ref([]);
 
+const group_list = ref([]);
+const show_tips_by_group = ref(false);
+const temp_group_list = ref([]);
+
 const filter_list = ref([]);
 
 const filter_type = ref('');
@@ -174,16 +235,17 @@ const onDrop = (event) => {
   if (event.dataTransfer.types.includes('application/x-dimension')) {
     rawData = event.dataTransfer.getData('application/x-dimension');
   }
-  console.log('rawData', rawData);
+  // console.log('rawData', rawData);
   // rawData = event.dataTransfer.getData('text/plain');
 
   if (!rawData) return;
+  if (measure_name.value == '') return Message.error('请先选择指标');
   try {
     const item = JSON.parse(rawData);
-    console.log(item, 999);
+    // console.log(item, 999);
     if (item.filter_config) {
       const filter_config = JSON.parse(item.filter_config);
-      console.log(filter_config, 888);
+      // console.log(filter_config, 888);
       if (!filter_config.is_aggregatable) {
         return Message.error('该维度不支持聚合');
       }
@@ -198,7 +260,69 @@ const onDrop = (event) => {
     dimension_list.value.push(item);
 
     reportEditor.changeDimensionListConfigItem(item);
-    emit('content-changed', { dimension_list: dimension_list.value, filter_list: filter_list.value });
+    emit('content-changed', {
+      dimension_list: dimension_list.value,
+      filter_list: filter_list.value,
+      group_list: group_list.value
+    });
+  } catch (e) {
+    console.warn('Failed to parse dropped metric:', e);
+  }
+};
+const onDropGroup = (event) => {
+  event.preventDefault();
+  event.stopPropagation();
+  let rawData = null;
+  if (event.dataTransfer.types.includes('application/x-dimension')) {
+    rawData = event.dataTransfer.getData('application/x-dimension');
+  }
+
+  if (!rawData) return;
+  if (measure_name.value == '') return Message.error('请先选择指标');
+  if (!dimension_list.value.length) return Message.error('请先添加维度');
+  try {
+    const rawItem = JSON.parse(rawData);
+
+    // 检查维度是否已添加
+    const idx = dimension_list.value.findIndex((dimension) => dimension.field_key === rawItem.field_key);
+    if (idx == -1) {
+      Message.error('请先添加该维度,再添加分组维度');
+      return;
+    }
+
+    // 检查分组是否已存在
+    const index = group_list.value.findIndex((it) => it.field_name == rawItem.field_name);
+    if (index != -1) {
+      Message.warning('分组已存在');
+      return;
+    }
+
+    // 创建新对象,只提取需要的字段,避免循环引用
+    const groupItem = {
+      group_id: generateID(),
+      field_id: rawItem.id || rawItem.field_id || '',
+      field_name: rawItem.field_name || '',
+      field_key: rawItem.field_key || '',
+      field_type: rawItem.field_type || '',
+      field_description: rawItem.field_description || '',
+      top_n: ''
+    };
+    // 当往分组中拖入新的分组时,提示是否替换当前分组
+    if (group_list.value.length) {
+      show_tips_by_group.value = true;
+      temp_group_list.value = [{ ...groupItem }];
+      return;
+    }
+    reportEditor.changeGroupListConfig(groupItem);
+    group_list.value = [];
+    group_list.value.push(groupItem);
+    console.log(group_list.value, 890);
+    // 触发内容变更事件
+    emit('content-changed', {
+      dimension_list: dimension_list.value,
+      filter_list: filter_list.value,
+      group_list: [...group_list.value]
+    });
   } catch (e) {
     console.warn('Failed to parse dropped metric:', e);
   }
@@ -215,7 +339,7 @@ const onDropFilter = (event) => {
     rawData = event.dataTransfer.getData('application/x-metric');
   }
   if (!rawData) return;
-
+  if (measure_name.value == '') return Message.error('请先选择指标');
   try {
     const item = JSON.parse(rawData);
     // insertOption(item);
@@ -223,14 +347,36 @@ const onDropFilter = (event) => {
     filter_list.value.push(item);
     filter_type.value = item.field_type;
     setFilterConfig(item);
-    emit('content-changed', { dimension_list: dimension_list.value, filter_list: filter_list.value });
+    emit('content-changed', {
+      dimension_list: dimension_list.value,
+      filter_list: filter_list.value,
+      group_list: group_list.value
+    });
   } catch (e) {
     console.warn('Failed to parse dropped metric:', e);
   }
 };
 
+const handleGroupReplaceConfirm = () => {
+  reportEditor.changeGroupListConfig(temp_group_list.value[0]);
+  group_list.value = [];
+  group_list.value.push(...temp_group_list.value);
+  temp_group_list.value = [];
+  // 触发内容变更事件
+  emit('content-changed', {
+    dimension_list: dimension_list.value,
+    filter_list: filter_list.value,
+    group_list: [...group_list.value]
+  });
+  show_tips_by_group.value = false;
+};
+const closeGroupReplaceModal = () => {
+  show_tips_by_group.value = false;
+  temp_group_list.value = [];
+};
+
 const closeNumberModel = (filter_config) => {
-  console.log(filter_config, 112);
+  // console.log(filter_config, 112);
   filter_list.value.forEach((item) => {
     if (item.filter_id === filter_config.filter_id) {
       if (!item.metric_fields_item) {
@@ -242,7 +388,7 @@ const closeNumberModel = (filter_config) => {
   closeModel();
 };
 const closeDateModel = (filter_config) => {
-  console.log(filter_config, 112);
+  // console.log(filter_config, 112);
   filter_list.value.forEach((item) => {
     if (item.filter_id === filter_config.filter_id) {
       if (!item.metric_fields_item) {
@@ -254,7 +400,7 @@ const closeDateModel = (filter_config) => {
   closeModel();
 };
 const closeStringModel = (filter_config) => {
-  console.log(filter_config, 112);
+  // console.log(filter_config, 112);
   filter_list.value.forEach((item) => {
     if (item.filter_id === filter_config.filter_id) {
       if (!item.metric_fields_item) {
@@ -271,13 +417,13 @@ const closeModel = () => {
 };
 
 const handleFilterClick = (item) => {
-  console.log(item, 111111111111111);
+  // console.log(item, 111111111111111);
   filter_type.value = item.field_type;
-  console.log(item);
+  // console.log(item);
   setFilterConfig(item);
 };
 const setFilterConfig = (item) => {
-  console.log(item, item.field_type === 'date');
+  // console.log(item, item.field_type === 'date');
   if (item.field_type == 'str') {
     filter_config_str.value = JSON.parse(item.filter_config);
     filter_config_str.value.metric_fields_item = str_form_default_data;
@@ -293,23 +439,61 @@ const setFilterConfig = (item) => {
   filter_id.value = item.filter_id;
   filter_form_data.value = item.metric_fields_item || null;
 
-  console.log('asasasdasdasd', filter_list.value);
+  // console.log('asasasdasdasd', filter_list.value);
   // return;
 };
 const handleDimensionRemove = (dimesion_id) => {
   dimension_list.value = dimension_list.value.filter((item) => item.dimesion_id !== dimesion_id);
   reportEditor.changeDimensionListConfig(dimension_list.value);
-  emit('content-changed', { dimension_list: dimension_list.value, filter_list: filter_list.value });
+  emit('content-changed', {
+    dimension_list: dimension_list.value,
+    filter_list: filter_list.value,
+    group_list: group_list.value
+  });
+};
+const handleDimensionRemoveByDimGroup = (dimesion_id) => {
+  handleDimensionRemove(dimesion_id);
+  handleGroupRemove(dimesion_id);
+};
+
+const handleGroupRemove = (group_id) => {
+  reportEditor.deleteGroupListConfig();
+  group_list.value = [];
+  emit('content-changed', {
+    dimension_list: dimension_list.value,
+    filter_list: filter_list.value,
+    group_list: group_list.value
+  });
+};
+
+const handleUpdateTopN = ({ group_id, top_n }) => {
+  const index = group_list.value.findIndex((item) => item.group_id === group_id);
+  if (index !== -1) {
+    group_list.value[index] = {
+      ...group_list.value[index],
+      top_n
+    };
+    reportEditor.changeGroupListConfig(group_list.value[index]);
+    emit('content-changed', {
+      dimension_list: dimension_list.value,
+      filter_list: filter_list.value,
+      group_list: group_list.value
+    });
+  }
 };
 
 const handleFilterRemove = (filter_id) => {
   filter_list.value = filter_list.value.filter((item) => item.filter_id !== filter_id);
   reportEditor.changeFilterListConfig(filter_list.value);
-  emit('content-changed', { dimension_list: dimension_list.value, filter_list: filter_list.value });
+  emit('content-changed', {
+    dimension_list: dimension_list.value,
+    filter_list: filter_list.value,
+    group_list: group_list.value
+  });
 };
 // 数字过滤器配置项
 const handleNumberConfirm = (filter_config) => {
-  console.log(filter_config);
+  // console.log(filter_config);
   filter_list.value.forEach((item) => {
     if (item.filter_id === filter_config.filter_id) {
       item.metric_fields_item = filter_config;
@@ -317,11 +501,15 @@ const handleNumberConfirm = (filter_config) => {
     }
   });
   closeModel();
-  emit('content-changed', { dimension_list: dimension_list.value, filter_list: filter_list.value });
+  emit('content-changed', {
+    dimension_list: dimension_list.value,
+    filter_list: filter_list.value,
+    group_list: group_list.value
+  });
 };
 // 字符串过滤器配置项
 const handleStringConfirm = (filter_config) => {
-  console.log(filter_config);
+  // console.log(filter_config);
   filter_list.value.forEach((item) => {
     if (item.filter_id === filter_config.filter_id) {
       item.metric_fields_item = filter_config;
@@ -329,11 +517,15 @@ const handleStringConfirm = (filter_config) => {
     }
   });
   closeModel();
-  emit('content-changed', { dimension_list: dimension_list.value, filter_list: filter_list.value });
+  emit('content-changed', {
+    dimension_list: dimension_list.value,
+    filter_list: filter_list.value,
+    group_list: group_list.value
+  });
 };
 // 日期过滤器配置项
 const handleDateConfirm = (filter_config) => {
-  console.log(filter_config);
+  // console.log(filter_config);
   filter_list.value.forEach((item) => {
     if (item.filter_id === filter_config.filter_id) {
       item.metric_fields_item = filter_config;
@@ -341,7 +533,11 @@ const handleDateConfirm = (filter_config) => {
     }
   });
   closeModel();
-  emit('content-changed', { dimension_list: dimension_list.value, filter_list: filter_list.value });
+  emit('content-changed', {
+    dimension_list: dimension_list.value,
+    filter_list: filter_list.value,
+    group_list: group_list.value
+  });
 };
 
 const handleUpdate = () => {
@@ -365,7 +561,7 @@ const handleCommonConfigClick = (item) => {
   common_config_list.value.forEach((it) => {
     if (it.field_key === item.field_key) {
       it.is_extend = item.is_checked ? 0 : 1;
-      it.is_checked = !item.is_checked;
+      it.is_checked = item.is_checked;
     }
   });
   reportEditor.changeCommonConfigListConfig(common_config_list.value);
@@ -373,11 +569,11 @@ const handleCommonConfigClick = (item) => {
 watch(
   [() => props.filter_option, () => reportEditor.QueryTargetDimensionListConfig],
   ([newVal, newTargetDiemensionList]) => {
-    console.log('newVal', newVal);
-    const dimension_query_list = newVal.option.source.flexible_template_metric_fields.filter(
+    console.log('newVal', newVal, newTargetDiemensionList);
+    const dimension_query_list = newVal?.option?.source?.flexible_template_metric_fields?.filter(
       (item) => item.column_role === 'dim'
     );
-    const result = dimension_query_list.filter((item) => newTargetDiemensionList.includes(item.field_key));
+    const result = dimension_query_list?.filter((item) => newTargetDiemensionList.includes(item.field_key)) || [];
     // 深拷贝result数组中的每个item,避免直接修改props
     const processedResult = result.map((item) => {
       return {
@@ -385,7 +581,33 @@ watch(
         dimesion_id: generateID()
       };
     });
+    console.log(processedResult, 9090);
     dimension_list.value = [...processedResult];
+    // 当往维度中拖入时,如果存在已有分组,则不更新分组
+    if (group_list.value.length) return;
+    // 获取当前指标配置的分组信息
+    let group_filed_key = reportEditor.QueryTargetGroupListConfig;
+    let group_item = {};
+    if (!dimension_list.value.length) return;
+    dimension_list.value.forEach((item) => {
+      console.log(item, group_filed_key);
+
+      if (item.field_key === group_filed_key) {
+        group_item = {
+          ...item,
+          group_id: generateID(),
+          top_n: ''
+        };
+        group_list.value.push(group_item);
+      }
+    });
+
+    console.log(group_list.value, 9090);
+    if (group_list.value.length) {
+      group_list.value[0].top_n = reportEditor.QueryTargetTopNListConfig;
+    } else {
+      group_list.value = [];
+    }
   },
   {
     immediate: true,
@@ -396,9 +618,9 @@ watch(
 watch(
   [() => props.filter_option, () => reportEditor.QueryTargetFilterListConfig],
   ([newVal, newTargetFilterList]) => {
-    const filter_query_list = newVal.option.source.flexible_template_metric_fields;
+    const filter_query_list = newVal?.option?.source?.flexible_template_metric_fields || [];
     const result = [];
-    console.log(filter_query_list, newTargetFilterList);
+    // console.log(filter_query_list, newTargetFilterList);
     filter_query_list.forEach((item) => {
       newTargetFilterList.forEach((filterItem) => {
         if (item.field_key === filterItem.field_key) {
@@ -411,7 +633,7 @@ watch(
         }
       });
     });
-    console.log(result, 111);
+    // console.log(result, 111);
     // 深拷贝result数组中的每个item,避免直接修改props
     filter_list.value = [...result];
   },
@@ -428,7 +650,11 @@ watch(
     if (targetExtendCommonConfigInfo) {
       return;
     }
-    common_config_list.value = newVal.map((item) => ({
+    if (!newVal.length) {
+      return;
+    }
+
+    common_config_list.value = (newVal || []).map((item) => ({
       ...item,
       is_checked: item.is_extend / 1 === 1 ? false : true // 0: 不继承,选中,1: 继承,不选中
     }));
@@ -436,7 +662,7 @@ watch(
       ...item,
       is_extend: item.is_checked ? 0 : 1
     }));
-    console.log('common_config_list', common_config_list.value);
+    // console.log('common_config_list', common_config_list.value);
     reportEditor.changeCommonConfigListConfig(submit_common_config_list);
   },
   {
@@ -449,6 +675,9 @@ watch(
   [() => props.filter_option, () => reportEditor.QueryTargetExtendCommonConfigInfo],
   ([newVal, newTargetExtendCommonConfigInfo]) => {
     console.log(1111111111111111111, newTargetExtendCommonConfigInfo);
+    if (!props.target_common_config_info.length) {
+      return;
+    }
     // 确保每个item都有is_checked属性
     const processedConfig = newTargetExtendCommonConfigInfo.map((item) => ({
       ...item,
@@ -469,6 +698,7 @@ defineExpose({
 </script>
 <style scoped lang="css">
 .mention_filter {
+  width: 300px;
   height: 100%;
   border-bottom: 1px solid #d8d8d8;
 }
@@ -516,6 +746,10 @@ defineExpose({
   cursor: pointer;
   max-width: 100%;
 }
+.empty-tip {
+  font-size: 14px;
+  color: #666;
+}
 .measure-name {
   flex: 1 1 auto;
   min-width: 0; /* allow flex item to shrink for ellipsis */
@@ -533,8 +767,8 @@ defineExpose({
   color: #0a817e;
 }
 .dimension {
-  height: 240px;
-  max-height: 240px;
+  height: 180px;
+  max-height: 180px;
   box-sizing: border-box;
 }
 .dimension-list {
@@ -571,8 +805,23 @@ defineExpose({
 .filter {
   height: calc(100% - 100px - 240px - 30px - 200px);
 }
+.filter .title {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.filter .title .tools {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+.filter .title .tools .icon {
+  width: 18px;
+  height: 18px;
+  cursor: pointer;
+}
 .filter-config {
-  height: 200px;
+  height: 150px;
   padding: 10px;
   box-sizing: border-box;
 }
@@ -622,6 +871,36 @@ defineExpose({
 .empty-tip {
   text-align: center;
 }
+.group {
+  border-top: 1px solid #ebeef5;
+  padding: 10px;
+  height: 140px;
+  max-height: 140px;
+  box-sizing: border-box;
+}
+.group-list {
+  min-height: 36px;
+  border-radius: 6px;
+  opacity: 1;
+  border: 1px dashed rgba(147, 147, 147, 0.6);
+  max-height: calc(100% - 50px);
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+  margin: 10px 0px;
+  padding: 5px 3px;
+  box-sizing: border-box;
+  /* overflow-y: auto; */
+}
+.filter-time {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 8px 10px;
+  background: #fafafa;
+  border-top: 1px solid #ebeef5;
+  font-size: 13px;
+}
 :deep(.arco-checkbox-checked .arco-checkbox-icon) {
   background-color: var(--primary-default);
 }

+ 48 - 7
src/modules/report-template/filter-model/model/DateModel.vue

@@ -140,6 +140,9 @@ const disabledDate = (current) => {
   const yesterday = new Date(now);
   yesterday.setDate(yesterday.getDate() - 1);
 
+  const dayBeforeYesterday = new Date(now);
+  dayBeforeYesterday.setDate(dayBeforeYesterday.getDate() - 2);
+
   // 重置时间部分,只比较日期
   const currentDate = new Date(current);
   currentDate.setHours(0, 0, 0, 0);
@@ -147,6 +150,8 @@ const disabledDate = (current) => {
   todayDate.setHours(0, 0, 0, 0);
   const yesterdayDate = new Date(yesterday);
   yesterdayDate.setHours(0, 0, 0, 0);
+  const dayBeforeYesterdayDate = new Date(dayBeforeYesterday);
+  dayBeforeYesterdayDate.setHours(0, 0, 0, 0);
 
   switch (timeLimit) {
     case 'limit_to_day_start':
@@ -157,6 +162,10 @@ const disabledDate = (current) => {
     case 'limit_to_yesterday_end':
       // 最多选到昨天
       return currentDate > yesterdayDate;
+    case 'limit_to_daybeforeyesterday_start':
+    case 'limit_to_daybeforeyesterday_end':
+      // 最多选到前天
+      return currentDate > dayBeforeYesterdayDate;
     default:
       return false;
   }
@@ -178,12 +187,18 @@ const disabledTime = (date) => {
   yesterdayDate.setDate(yesterdayDate.getDate() - 1);
   yesterdayDate.setHours(0, 0, 0, 0);
 
+  const dayBeforeYesterdayDate = new Date(now);
+  dayBeforeYesterdayDate.setDate(dayBeforeYesterdayDate.getDate() - 2);
+  dayBeforeYesterdayDate.setHours(0, 0, 0, 0);
+
   // 检查是否是限制的最后一天
   const isLastDay =
     ((timeLimit === 'limit_to_day_start' || timeLimit === 'limit_to_day_end') &&
       selectedDate.getTime() === todayDate.getTime()) ||
     ((timeLimit === 'limit_to_yesterday_start' || timeLimit === 'limit_to_yesterday_end') &&
-      selectedDate.getTime() === yesterdayDate.getTime());
+      selectedDate.getTime() === yesterdayDate.getTime()) ||
+    ((timeLimit === 'limit_to_daybeforeyesterday_start' || timeLimit === 'limit_to_daybeforeyesterday_end') &&
+      selectedDate.getTime() === dayBeforeYesterdayDate.getTime());
 
   if (!isLastDay) {
     return {};
@@ -204,12 +219,22 @@ const disabledTime = (date) => {
         disabledMinutes: () => Array.from({ length: 60 }, (_, i) => i).filter((minute) => minute > 0),
         disabledSeconds: () => Array.from({ length: 60 }, (_, i) => i).filter((second) => second > 0)
       };
+    case 'limit_to_daybeforeyesterday_start':
+      // 最多选到前天的 00:00:00
+      return {
+        disabledHours: () => Array.from({ length: 24 }, (_, i) => i).filter((hour) => hour > 0),
+        disabledMinutes: () => Array.from({ length: 60 }, (_, i) => i).filter((minute) => minute > 0),
+        disabledSeconds: () => Array.from({ length: 60 }, (_, i) => i).filter((second) => second > 0)
+      };
     case 'limit_to_day_end':
       // 最多选到今天的 23:59:59
       return {};
     case 'limit_to_yesterday_end':
       // 最多选到昨天的 23:59:59
       return {};
+    case 'limit_to_daybeforeyesterday_end':
+      // 最多选到前天的 23:59:59
+      return {};
     default:
       return {};
   }
@@ -231,12 +256,18 @@ const disabledRangeTime = (date, type) => {
   yesterdayDate.setDate(yesterdayDate.getDate() - 1);
   yesterdayDate.setHours(0, 0, 0, 0);
 
+  const dayBeforeYesterdayDate = new Date(now);
+  dayBeforeYesterdayDate.setDate(dayBeforeYesterdayDate.getDate() - 2);
+  dayBeforeYesterdayDate.setHours(0, 0, 0, 0);
+
   // 检查是否是限制的最后一天
   const isLastDay =
     ((timeLimit === 'limit_to_day_start' || timeLimit === 'limit_to_day_end') &&
       selectedDate.getTime() === todayDate.getTime()) ||
     ((timeLimit === 'limit_to_yesterday_start' || timeLimit === 'limit_to_yesterday_end') &&
-      selectedDate.getTime() === yesterdayDate.getTime());
+      selectedDate.getTime() === yesterdayDate.getTime()) ||
+    ((timeLimit === 'limit_to_daybeforeyesterday_start' || timeLimit === 'limit_to_daybeforeyesterday_end') &&
+      selectedDate.getTime() === dayBeforeYesterdayDate.getTime());
 
   if (!isLastDay) {
     return {};
@@ -257,12 +288,22 @@ const disabledRangeTime = (date, type) => {
         disabledMinutes: () => Array.from({ length: 60 }, (_, i) => i).filter((minute) => minute > 0),
         disabledSeconds: () => Array.from({ length: 60 }, (_, i) => i).filter((second) => second > 0)
       };
+    case 'limit_to_daybeforeyesterday_start':
+      // 最多选到前天的 00:00:00
+      return {
+        disabledHours: () => Array.from({ length: 24 }, (_, i) => i).filter((hour) => hour > 0),
+        disabledMinutes: () => Array.from({ length: 60 }, (_, i) => i).filter((minute) => minute > 0),
+        disabledSeconds: () => Array.from({ length: 60 }, (_, i) => i).filter((second) => second > 0)
+      };
     case 'limit_to_day_end':
       // 最多选到今天的 23:59:59
       return {};
     case 'limit_to_yesterday_end':
       // 最多选到昨天的 23:59:59
       return {};
+    case 'limit_to_daybeforeyesterday_end':
+      // 最多选到前天的 23:59:59
+      return {};
     default:
       return {};
   }
@@ -280,7 +321,7 @@ const rangeTypeConfig = computed(() => {
 // 3. 【关键】初始化表单(避免在 reactive 中直接用 computed)
 const initForm = () => {
   const f = props.filter || {};
-  console.log('initForm', f);
+  // console.log('initForm', f);
 
   // 字段基础信息
   formModel.value.field_name = f.field_name || '字段名称';
@@ -370,8 +411,8 @@ const handleBeforeOk = (done) => {
     submitData.field_value = [...data.dateRange];
   }
   submitData.filter_id = props.filter_id;
-  console.log('提交数据:', submitData);
-  console.log('提交数据:', JSON.stringify(submitData));
+  // console.log('提交数据:', submitData);
+  // console.log('提交数据:', JSON.stringify(submitData));
   emit('confirm', submitData);
   done();
 };
@@ -404,8 +445,8 @@ const handleCancel = () => {
     submitData.field_value = [...data.dateRange];
   }
   submitData.filter_id = props.filter_id;
-  console.log('提交数据:', submitData);
-  console.log('提交数据:', JSON.stringify(submitData));
+  // console.log('提交数据:', submitData);
+  // console.log('提交数据:', JSON.stringify(submitData));
   emit('cancel', submitData);
 };
 </script>