瀏覽代碼

feat:新增数据源复制,指标复制,数据源过滤器增加精确到年月,指标中支持不聚会,不配置同环比

韩洋 4 月之前
父節點
當前提交
463e4c1aaa

+ 26 - 3
src/base/store/use-report-editor.js

@@ -86,6 +86,29 @@ export const useReportEditor = defineStore('report-editor', {
     setTargetMetricId(id) {
       this.target_metric_id = id;
     },
+
+    // 复制指标配置:根据旧 uid,把除 uid/index/option 外的配置复制到新 uid 对应项
+    copyMetricConfigByUid(oldUid, newUid) {
+      if (!oldUid || !newUid) return;
+      const oldItem = this.metrics_list.find((it) => it && it.index === oldUid);
+      const newItem = this.metrics_list.find((it) => it && it.index === newUid);
+      if (!oldItem || !newItem) return;
+
+      const preserved = {
+        uid: newItem.uid,
+        index: newItem.index,
+        option: newItem.option
+      };
+
+      // 深拷贝旧配置,避免引用同一个对象导致联动修改
+      const clonedOld = JSON.parse(JSON.stringify(oldItem));
+      Object.assign(newItem, clonedOld);
+
+      // 恢复新 uid 的身份标识与 option(option 本身已经在 mention 插入时复制过)
+      newItem.uid = preserved.uid;
+      newItem.index = preserved.index;
+      newItem.option = preserved.option;
+    },
     syncArraysById(old_list, new_list, idKey = 'uid') {
       // 对新列表进行去重,确保 new_list 中没有重复的 id
       const uniqueNewList = [];
@@ -150,7 +173,7 @@ export const useReportEditor = defineStore('report-editor', {
           "default_metric_field_key": '', // 默认指标字段key, 如果指标配置中没有配置指标字段key,则默认使用默认指标字段key
           "format_field_key": '', // 指标字段key
           "yoy_or_mom": '', /// 同比环比
-          "aggregation_type": 'sum', /// 聚合方式
+          "aggregation_type": '', /// 聚合方式
         }
       }
 
@@ -200,7 +223,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: item.metric_config.aggregation_type || 'sum',
+              aggs_type: item.metric_config.aggregation_type || '',
               aggs_field_key: item.option.field_key,
               group_by_field_key: [],
             }
@@ -432,7 +455,7 @@ export const useReportEditor = defineStore('report-editor', {
     },
     // 获取当前度量的聚合方式
     QueryTargetAggregationType: (state) => {
-      return state.metrics_list.find(item => item.index === state.target_metric_id)?.metric_config?.aggregation_type || 'sum'
+      return state.metrics_list.find(item => item.index === state.target_metric_id)?.metric_config?.aggregation_type || ''
     },
   },
 });

+ 14 - 4
src/modules/data-source-manage/add-source/AddSourceModel.vue

@@ -4,7 +4,7 @@
     <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>
+      <AddSourceHeader :type="type" @to_back="toBack" />
       <div class="container-body">
         <BasicInfo v-model="formData.basicInfo" @type-change="handleTypeChange" />
 
@@ -17,7 +17,7 @@
         <DbFieldConfig v-else-if="dataSourceType === '2'" v-model="formData.dbFieldConfig" />
 
         <div class="save-btn-container">
-          <a-button type="primary" @click="handleSaveAll">全部保存</a-button>
+          <a-button type="primary" @click="handleSaveAll"> 全部保存 </a-button>
         </div>
       </div>
     </div>
@@ -71,7 +71,12 @@ const formData = ref({
     api_method: 'GET',
     api_params: [],
     data_level: '',
-    data_result_type: 'list'
+    data_result_type: 'list',
+    api_req_page_field: '',
+    api_req_pagesize_field: '',
+    api_res_pages_field: '',
+    api_res_pagesize_field: '',
+    api_res_total_field: ''
   },
   dbConfig: {
     db_sql_template: '',
@@ -169,7 +174,12 @@ const getDataSourceItemDetailFunc = async (data) => {
           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'
+          data_result_type: allData.metric_config?.data_result_type || 'list',
+          api_req_page_field: allData.metric_config?.api_req_page_field || '',
+          api_req_pagesize_field: allData.metric_config?.api_req_pagesize_field || '',
+          api_res_pages_field: allData.metric_config?.api_res_pages_field || '',
+          api_res_pagesize_field: allData.metric_config?.api_res_pagesize_field || '',
+          api_res_total_field: allData.metric_config?.api_res_total_field || ''
         };
 
         // 回填API字段配置

+ 89 - 12
src/modules/data-source-manage/add-source/components/ApiConfig.vue

@@ -4,16 +4,16 @@
     <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-input v-model="api_url" placeholder="/zbgkht_tz/zbgk/queryGwtbzbDataThree" @input="handleApiUrlChange" />
         </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-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>
@@ -25,21 +25,21 @@
             <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"
+                @input="handleParamChange"
               />
               <a-input
                 v-model="param.value"
-                @input="handleParamChange"
                 placeholder="参数值"
                 style="flex: 1; margin-right: 10px"
+                @input="handleParamChange"
               />
               <a-button type="outline" status="danger" @click="removeParam(param.id)">
                 <icon-delete />
               </a-button>
             </div>
-            <a-button type="outline" @click="addParam">+ 添加参数</a-button>
+            <a-button type="outline" @click="addParam"> + 添加参数 </a-button>
           </div>
         </a-form-item>
       </div>
@@ -47,18 +47,60 @@
     <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-input v-model="data_level" placeholder="数据级别" @input="handleDataLevelChange" />
         </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-option value="list"> 列表 </a-option>
+            <a-option value="dict"> 字典 </a-option>
           </a-select>
         </a-form-item>
       </div>
     </div>
+
+    <div class="form-row">
+      <div class="form-item">
+        <a-form-item field="api_req_page_field" label="接口参数页数">
+          <a-input v-model="api_req_page_field" placeholder="接口参数页数" @input="handleApiReqPageFieldChange" />
+        </a-form-item>
+      </div>
+      <div class="form-item">
+        <a-form-item field="api_req_pagesize_field" label="接口参数pagesize">
+          <a-input
+            v-model="api_req_pagesize_field"
+            placeholder="接口参数pagesize"
+            @input="handleApiReqPageSizeFieldChange"
+          />
+        </a-form-item>
+      </div>
+    </div>
+
+    <div class="form-row">
+      <div class="form-item">
+        <a-form-item field="api_res_pages_field" label="接口返回总页数">
+          <a-input v-model="api_res_pages_field" placeholder="接口返回总页数" @input="handleApiResPagesFieldChange" />
+        </a-form-item>
+      </div>
+      <div class="form-item">
+        <a-form-item field="api_res_pagesize_field" label="接口返回pagesize">
+          <a-input
+            v-model="api_res_pagesize_field"
+            placeholder="接口返回pagesize"
+            @input="handleApiResPageSizeFieldChange"
+          />
+        </a-form-item>
+      </div>
+    </div>
+
+    <div class="form-row">
+      <div class="form-item full-width">
+        <a-form-item field="api_res_total_field" label="接口返回total">
+          <a-input v-model="api_res_total_field" placeholder="接口返回total" @input="handleApiResTotalFieldChange" />
+        </a-form-item>
+      </div>
+    </div>
   </div>
 </template>
 
@@ -80,6 +122,11 @@ 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');
+const api_req_page_field = ref(props.modelValue.api_req_page_field || '');
+const api_req_pagesize_field = ref(props.modelValue.api_req_pagesize_field || '');
+const api_res_pages_field = ref(props.modelValue.api_res_pages_field || '');
+const api_res_pagesize_field = ref(props.modelValue.api_res_pagesize_field || '');
+const api_res_total_field = ref(props.modelValue.api_res_total_field || '');
 
 // 监听props变化,更新本地数据
 watch(() => props.modelValue, (newValue) => {
@@ -88,6 +135,11 @@ watch(() => props.modelValue, (newValue) => {
   api_params.value = newValue.api_params || [];
   data_level.value = newValue.data_level || '';
   data_result_type.value = newValue.data_result_type || 'list';
+  api_req_page_field.value = newValue.api_req_page_field || '';
+  api_req_pagesize_field.value = newValue.api_req_pagesize_field || '';
+  api_res_pages_field.value = newValue.api_res_pages_field || '';
+  api_res_pagesize_field.value = newValue.api_res_pagesize_field || '';
+  api_res_total_field.value = newValue.api_res_total_field || '';
 }, { deep: true });
 
 // 通知父组件数据变化
@@ -97,7 +149,12 @@ const notifyParent = () => {
     api_method: api_method.value,
     api_params: api_params.value,
     data_level: data_level.value,
-    data_result_type: data_result_type.value
+    data_result_type: data_result_type.value,
+    api_req_page_field: api_req_page_field.value,
+    api_req_pagesize_field: api_req_pagesize_field.value,
+    api_res_pages_field: api_res_pages_field.value,
+    api_res_pagesize_field: api_res_pagesize_field.value,
+    api_res_total_field: api_res_total_field.value
   });
 };
 
@@ -118,6 +175,26 @@ const handleDataResultTypeChange = () => {
   notifyParent();
 };
 
+const handleApiReqPageFieldChange = () => {
+  notifyParent();
+};
+
+const handleApiReqPageSizeFieldChange = () => {
+  notifyParent();
+};
+
+const handleApiResPagesFieldChange = () => {
+  notifyParent();
+};
+
+const handleApiResPageSizeFieldChange = () => {
+  notifyParent();
+};
+
+const handleApiResTotalFieldChange = () => {
+  notifyParent();
+};
+
 const handleParamChange = () => {
   notifyParent();
 };

+ 49 - 34
src/modules/data-source-manage/add-source/components/ApiFieldConfig.vue

@@ -3,7 +3,7 @@
     <div class="section-header">
       <h3 class="section-title">字段配置</h3>
       <div class="add-field-btn-container">
-        <a-button type="primary" @click="addField">+ 添加字段</a-button>
+        <a-button type="primary" @click="addField"> + 添加字段 </a-button>
       </div>
     </div>
     <div class="table-wrapper">
@@ -31,94 +31,101 @@
         </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="字段描述" />
+              <a-input v-model="field.field_name" placeholder="字段名称" @input="handleFieldChange" />
+            </td>
+            <td>
+              <a-input v-model="field.field_description" placeholder="字段描述" @input="handleFieldChange" />
             </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-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-input v-model="field.field_key" placeholder="字段Key" @input="handleFieldChange" />
+            </td>
             <td>
               <a-select v-model="field.column_role" @change="handleFieldChange">
-                <a-option value="dim">维度</a-option>
-                <a-option value="meas">度量</a-option>
+                <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-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-option :value="0">  </a-option>
+                <a-option :value="1">  </a-option>
               </a-select>
             </td>
             <td>
               <a-select v-model="field.is_support_api_query_condition" @change="handleFieldChange">
-                <a-option :value="0">否</a-option>
-                <a-option :value="1">是</a-option>
+                <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="通用配置类型" />
+              <a-input v-model="field.common_config_type" placeholder="通用配置类型" @input="handleFieldChange" />
             </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-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-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-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="多值分隔符" />
+              <a-input v-model="field.multi_value_delimiter" placeholder="多值分隔符" @input="handleFieldChange" />
             </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-option :value="0">  </a-option>
+                <a-option :value="1">  </a-option>
               </a-select>
             </td>
             <td>
-              <a-select v-model="field.format_str" @change="handleFieldChange" :disabled="field.field_type !== 'date'">
-                <a-option value="date">精确到日期</a-option>
-                <a-option value="datetime">精确到时间</a-option>
+              <a-select v-model="field.format_str" :disabled="field.field_type !== 'date'" @change="handleFieldChange">
+                <a-option value="year_month"> 精确到年月 </a-option>
+                <a-option value="date"> 精确到日期 </a-option>
+                <a-option value="datetime"> 精确到时间 </a-option>
               </a-select>
             </td>
             <td>
               <a-select
                 v-model="field.time_limit"
-                @change="handleFieldChange"
                 :disabled="field.field_type !== 'date'"
                 allow-clear
                 :style="{ width: '170px' }"
+                @change="handleFieldChange"
               >
-                <a-option value="limit_to_yesterday_start">昨天的 00:00:00</a-option>
-                <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_yesterday_start"> 昨天的 00:00:00 </a-option>
+                <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-select>
             </td>
             <td>
-              <a-button type="outline" status="danger" @click="deleteField(index)"><icon-delete /></a-button>
+              <a-button type="outline" status="danger" @click="deleteField(index)">
+                <icon-delete />
+              </a-button>
             </td>
           </tr>
         </tbody>
@@ -225,6 +232,8 @@ const deleteField = (index) => {
 
 .table-wrapper {
   overflow-x: auto;
+  overflow-y: auto;
+  max-height: 300px;
   margin-bottom: 16px;
 }
 
@@ -234,6 +243,12 @@ const deleteField = (index) => {
   min-width: 1200px;
 }
 
+.field-table thead th {
+  position: sticky;
+  top: 0;
+  z-index: 2;
+}
+
 .field-table th,
 .field-table td {
   padding: 8px;

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

@@ -23,7 +23,7 @@
         <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-option value="dict">字典</a-option>
           </a-select>
         </a-form-item>
       </div>

+ 43 - 28
src/modules/data-source-manage/add-source/components/DbFieldConfig.vue

@@ -3,7 +3,7 @@
     <div class="section-header">
       <h3 class="section-title">字段配置</h3>
       <div class="add-field-btn-container">
-        <a-button type="primary" @click="addField">+ 添加字段</a-button>
+        <a-button type="primary" @click="addField"> + 添加字段 </a-button>
       </div>
     </div>
     <div class="table-wrapper">
@@ -28,77 +28,84 @@
         </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="字段描述" />
+              <a-input v-model="field.field_name" placeholder="字段名称" @input="handleFieldChange" />
+            </td>
+            <td>
+              <a-input v-model="field.field_description" placeholder="字段描述" @input="handleFieldChange" />
             </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-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-input v-model="field.field_key" placeholder="字段Key" @input="handleFieldChange" />
+            </td>
             <td>
               <a-select v-model="field.column_role" @change="handleFieldChange">
-                <a-option value="dim">维度</a-option>
-                <a-option value="meas">度量</a-option>
+                <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-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-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="通用配置类型" />
+              <a-input v-model="field.common_config_type" placeholder="通用配置类型" @input="handleFieldChange" />
             </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-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="字段来源表" />
+              <a-input v-model="field.field_from_table" placeholder="字段来源表" @input="handleFieldChange" />
             </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-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-select v-model="field.format_str" @change="handleFieldChange" :disabled="field.field_type !== 'date'">
-                <a-option value="date">精确到日期</a-option>
-                <a-option value="datetime">精确到时间</a-option>
+              <a-select v-model="field.format_str" :disabled="field.field_type !== 'date'" @change="handleFieldChange">
+                <a-option value="year_month"> 精确到年月 </a-option>
+                <a-option value="date"> 精确到日期 </a-option>
+                <a-option value="datetime"> 精确到时间 </a-option>
               </a-select>
             </td>
             <td>
               <a-select
                 v-model="field.time_limit"
                 :style="{ width: '160px' }"
-                @change="handleFieldChange"
                 :disabled="field.field_type !== 'date'"
                 allow-clear
+                @change="handleFieldChange"
               >
-                <a-option value="limit_to_yesterday_start">昨天的 00:00:00</a-option>
-                <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_yesterday_start"> 昨天的 00:00:00 </a-option>
+                <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-select>
             </td>
             <td>
-              <a-button type="outline" status="danger" @click="deleteField(index)"><icon-delete /></a-button>
+              <a-button type="outline" status="danger" @click="deleteField(index)">
+                <icon-delete />
+              </a-button>
             </td>
           </tr>
         </tbody>
@@ -202,6 +209,8 @@ const deleteField = (index) => {
 
 .table-wrapper {
   overflow-x: auto;
+  overflow-y: auto;
+  max-height: 300px;
   margin-bottom: 16px;
 }
 
@@ -211,6 +220,12 @@ const deleteField = (index) => {
   min-width: 1200px;
 }
 
+.field-table thead th {
+  position: sticky;
+  top: 0;
+  z-index: 2;
+}
+
 .field-table th,
 .field-table td {
   padding: 8px;

+ 126 - 0
src/modules/data-source-manage/data-source/CopySource.vue

@@ -0,0 +1,126 @@
+<!--
+ * @Author: 韩洋 1528276425@qq.com
+ * @Date: 2026-04-17 11:22:54
+ * @LastEditors: 韩洋 1528276425@qq.com
+ * @LastEditTime: 2026-04-17 11:55:12
+ * @FilePath: \smartai_desk_pc\src\modules\data-source-manage\data-source\CopySource.vue
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+-->
+<template>
+  <a-modal :visible="visible" title="复制数据源" :mask-closable="false" @cancel="handleCancel" @ok="handleSave">
+    <a-form ref="formRef" :model="form" layout="">
+      <a-form-item
+        field="metric_name"
+        label="数据源名称"
+        :rules="[{ required: true, message: '请输入数据源名称' }]"
+        validate-trigger="input"
+      >
+        <a-input v-model="form.metric_name" placeholder="请输入数据源名称" />
+      </a-form-item>
+    </a-form>
+
+    <template #footer>
+      <div class="footer-actions">
+        <a-button @click="handleCancel"> 取消 </a-button>
+        <a-button type="primary" @click="handleSave"> 保存 </a-button>
+      </div>
+    </template>
+  </a-modal>
+</template>
+
+<script setup lang="js">
+import { reactive, ref, watch,onMounted } from 'vue';
+import { createOrUpdateMetric,getDataSourceItemDetail} from '../../conversation-dialog/api-template.js'
+import { Message } from '@arco-design/web-vue';
+
+const props = defineProps({
+  visible: {
+    type: Boolean,
+    default: false
+  },
+  copy_id: {
+    type: String,
+    default: ''
+  }
+});
+
+const emit = defineEmits(['update:visible', 'save', 'cancel']);
+
+const formRef = ref();
+const form = reactive({
+  metric_name: ''
+});
+const new_source_data = ref({});
+
+watch(
+  () => [props.visible, props.defaultName],
+  ([v]) => {
+    if (v) {
+      form.metric_name = props.defaultName || '';
+      formRef.value?.clearValidate?.();
+    }
+  },
+  { immediate: true }
+);
+
+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);
+      new_source_data.value = allData;
+    }
+  }catch(error){
+    console.error('获取数据源详情失败:', error);
+  }
+};
+const handleCancel = () => {
+  emit('update:visible', false);
+  emit('cancel');
+};
+
+const handleSave = async () => {
+  const errors = await formRef.value?.validate?.();
+  if (errors) return;
+
+  try{
+    console.log('保存数据源:', new_source_data.value);
+    new_source_data.value.metric_config.metric_name = form.metric_name
+    console.log(new_source_data.value)
+    const data = {
+      config:{
+        ...new_source_data.value
+      }
+    }
+    const res = await createOrUpdateMetric(data)
+    if(res.code/1 === 200){
+      Message.success('复制成功');
+      emit('save');
+      emit('update:visible', false);
+    }else {
+      Message.error('复制失败');
+    }
+
+  }catch(error){
+    Message.error('复制失败');
+    console.error('保存数据源失败:', error);
+  }
+
+
+};
+
+onMounted(() => {
+  if(props.copy_id){
+    getDataSourceItemDetailFunc({metric_id: props.copy_id});
+  }
+});
+</script>
+
+<style scoped>
+.footer-actions {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+}
+</style>

+ 40 - 16
src/modules/data-source-manage/data-source/DataSourceBody.vue

@@ -1,10 +1,10 @@
 <template>
   <div class="detail-body">
     <a-table
+      v-model:selected-keys="selectedKeys"
       :row-selection="rowSelection"
-      v-model:selectedKeys="selectedKeys"
       :pagination="false"
-      rowKey="id"
+      row-key="id"
       :data="groupTemplates"
       :loading="is_Loading"
       :scroll="{ y: 'calc(100% - 64px)' }"
@@ -21,21 +21,22 @@
           :width="item.width"
         >
           <template #cell="{ record, column }">
-            <div class="column" v-if="item.dataIndex === 'name'">
+            <div v-if="item.dataIndex === 'name'" class="column">
               <img src="../../../assets/report-template/template_name.svg" alt="" />
               {{ record[column.dataIndex] }}
             </div>
-            <div class="column" v-else-if="item.dataIndex === 'data_source_type'">
+            <div v-else-if="item.dataIndex === 'data_source_type'" class="column">
               {{ record[column.dataIndex] / 1 === 1 ? 'API' : '数据库' }}
             </div>
-            <div class="column" v-else-if="item.dataIndex === 'created_at'">
+            <div v-else-if="item.dataIndex === 'created_at'" class="column">
               {{ formatDateTime(record[column.dataIndex]) || '-' }}
             </div>
-            <div class="column" v-else-if="item.dataIndex === 'action'">
+            <div v-else-if="item.dataIndex === 'action'" class="column">
+              <span class="link" @click="handleCopy(record)">复制</span>
               <span class="link" @click="handleEdit(record)">编辑</span>
               <span class="link link_del" @click="handleDelete(record)">删除</span>
             </div>
-            <div class="column" v-else>
+            <div v-else class="column">
               {{ record[column.dataIndex] }}
             </div>
           </template>
@@ -47,25 +48,28 @@
         :total="page.total"
         :current="page.current"
         :page-size="page.page_size"
-        @change="changePageChange"
-        @page-size-change="changePageSizeChange"
         show-total
         show-page-size
+        @change="changePageChange"
+        @page-size-change="changePageSizeChange"
       />
     </div>
   </div>
-  <DeleteModal
-    :visible="show_delete_modal"
-    :is_branch="is_branch"
-    @ok="handleConfirm"
-    @cancel="handleCancel"
-  ></DeleteModal>
+  <DeleteModal :visible="show_delete_modal" :is_branch="is_branch" @ok="handleConfirm" @cancel="handleCancel" />
+  <CopySource
+    v-if="show_copy_modal"
+    :visible="show_copy_modal"
+    :copy_id="target_copy_id"
+    @save="handleCopySave"
+    @cancel="handleCopyCancel"
+  />
 </template>
 
-<script setup >
+<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 CopySource from './CopySource.vue';
 import { Message } from '@arco-design/web-vue';
 import { formatDateTime } from '../../../utils/utils';
 
@@ -73,8 +77,10 @@ const emit = defineEmits(['edit-template']);
 
 const selectedKeys = ref([]);
 const show_delete_modal = ref(false);
+const show_copy_modal = ref(false);
 const is_branch = ref(false);
 const selected_id_list = ref([]);
+const target_copy_id = ref('');
 const filter_config = ref({
   filter: null,
   filter_value: null
@@ -283,6 +289,24 @@ const handleEdit = (record) => {
   emit('edit-datasource', record);
 };
 
+const handleCopy = (record) => {
+  show_copy_modal.value = true;
+  console.log(record);
+  target_copy_id.value = record.id;
+  // default_name.value = record.metric_name;
+};
+
+const handleCopyCancel = () => {
+  show_copy_modal.value = false;
+  target_copy_id.value = '';
+};
+
+const handleCopySave = () => {
+  show_copy_modal.value = false;
+  target_copy_id.value = '';
+  getDataSourceListFun();
+};
+
 const clearFilter = (params) => {
   filter_config.value = {
     filter: params.type,

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

@@ -189,6 +189,8 @@ const emit = defineEmits([
 const editorRef = ref(null);
 const containerRef = ref(null); // 👈 新增:用于定位浮层
 const mentionDropdownRef = ref(null);
+// 记录编辑器内“最后一次有效光标”,用于复制/插入等操作(避免点击菜单导致 selection 丢失)
+const lastEditorSelectionRange = ref(null);
 
 // 图表数据存储,使用chartId作为唯一标识
 const chartDataStore = ref({});
@@ -534,6 +536,14 @@ function handleSelectionChange() {
   if (editorRef.value && document.activeElement === editorRef.value) {
     updateToolbar();
   }
+  // 记录编辑器内的选区(用于复制指标时回到光标处插入)
+  const sel = window.getSelection();
+  if (sel?.rangeCount && editorRef.value) {
+    const r = sel.getRangeAt(0);
+    if (editorRef.value.contains(r.startContainer)) {
+      lastEditorSelectionRange.value = r.cloneRange();
+    }
+  }
 }
 
 // ============ 工具栏状态同步 ============
@@ -832,9 +842,47 @@ function triggerDropdown() {
   showDropdown.value = true;
 }
 
-function insertOption(opt) {
+function normalizeInsertRange() {
+  if (!editorRef.value) return null;
+  const editor = editorRef.value;
+  const sel = window.getSelection();
+
+  if (lastEditorSelectionRange.value && editor.contains(lastEditorSelectionRange.value.startContainer)) {
+    return lastEditorSelectionRange.value.cloneRange();
+  }
+  if (sel?.rangeCount && editor.contains(sel.getRangeAt(0).startContainer)) {
+    return sel.getRangeAt(0).cloneRange();
+  }
+
+  // fallback:末尾
+  const range = document.createRange();
+  const lastChild = editor.lastChild;
+  if (lastChild) {
+    range.selectNodeContents(lastChild);
+    range.collapse(false);
+  } else {
+    range.selectNodeContents(editor);
+    range.collapse(true);
+  }
+  return range;
+}
+
+function setSelectionToRange(range) {
+  if (!range) return;
+  const sel = window.getSelection();
+  if (!sel) return;
+  sel.removeAllRanges();
+  sel.addRange(range);
+}
+
+function insertOption(opt, options = {}) {
   const sel = window.getSelection();
-  if (sel.rangeCount === 0 || !editorRef.value) return;
+  if (!editorRef.value) return null;
+
+  // 复制场景:点击菜单可能会导致 selection 不在 editor 内,这里强制恢复到最后一次光标位置
+  const insertRange = options?.range || normalizeInsertRange();
+  setSelectionToRange(insertRange);
+  if (!sel?.rangeCount) return null;
 
   const range = sel.getRangeAt(0);
 
@@ -898,6 +946,7 @@ function insertOption(opt) {
   immediateSave(); // 👈 接入你的历史机制
   emit('submitHtml', editorRef.value.innerHTML);
   emit('content-changed', editorRef.value.innerHTML);
+  return uid;
 }
 // 当编辑器内容改变时,同步数据
 function syncMentionsData() {
@@ -1257,6 +1306,12 @@ function handleAction(action) {
   const mention = currentMentionElement.value;
   if (!mention || !editorRef.value) return;
 
+  // MentionActionMenu 既可能发字符串(如 'copy'),也可能发对象(配置变更)
+  if (typeof action === 'object' && action) {
+    // 这里是配置类菜单变更(TopN/排序/格式等),store 已在子组件里处理
+    return;
+  }
+
   switch (action) {
     case 'delete':
       mention.remove();
@@ -1283,6 +1338,29 @@ function handleAction(action) {
       const name = mention.textContent.slice(1, -1);
       alert(`查看详情: ${name}`);
       return;
+
+    case 'copy': {
+      const oldUid = mention.getAttribute('data-uid');
+      if (!oldUid) return;
+      const oldMentionData = mentionsData.value.find((it) => it && it.uid === oldUid);
+      if (!oldMentionData?.option) return;
+
+      // 在“最后一次光标位置”插入一份新的 mention
+      const clonedOpt = JSON.parse(JSON.stringify(oldMentionData.option));
+      const newUid = insertOption(clonedOpt, { range: normalizeInsertRange() });
+      if (!newUid) return;
+
+      nextTick(() => {
+        reportEditor.copyMetricConfigByUid(oldUid, newUid);
+        reportEditor.setTargetMetricId(newUid);
+      });
+
+      hideActionMenu();
+      immediateSave();
+      emit('submitHtml', editorRef.value.innerHTML);
+      emit('content-changed', editorRef.value.innerHTML);
+      return;
+    }
   }
 
   hideActionMenu();

+ 87 - 5
src/modules/report-template/editor-select/MentionActionMenu.vue

@@ -33,8 +33,8 @@
       >
         <!-- TopN 子菜单 -->
         <div
-          class="submenu-item has-children"
           v-if="actionMenuItems.top_n_config"
+          class="submenu-item has-children"
           @mouseenter="
             () => {
               openSubmenu('topn');
@@ -68,8 +68,8 @@
         </div>
         <!-- 同环比子菜单 -->
         <div
-          class="submenu-item has-children"
           v-if="actionMenuItems.yoy_or_mom_config"
+          class="submenu-item has-children"
           @mouseenter="
             () => {
               openSubmenu('yoymom');
@@ -196,13 +196,20 @@
         </button>
       </div>
     </div>
+
+    <div class="menu-divider"></div>
+
+    <!-- 复制:在富文本当前光标处插入相同指标(父组件处理) -->
+    <button type="button" class="menu-item plain" @click.stop="handleCopy">
+      <span>复制指标</span>
+    </button>
   </div>
   <CustomModal
     v-if="show_custom_modal"
     :show_model="show_custom_modal"
     :action_config="currentConfig"
     @cancel="handleClose"
-  ></CustomModal>
+  />
 </template>
 
 <script setup>
@@ -313,6 +320,11 @@ const actionMenuItems = ref({
   yoy_or_mom_config: {
     is_show: 1,
     children: [
+      {
+        name: '无',
+        value: '',
+        is_show: 1
+      },
       {
         name: '同比',
         value: 'yoy',
@@ -385,6 +397,11 @@ const actionMenuItems = ref({
   aggregation_config: {
     is_show: 1,
     children: [
+      {
+        name: '无聚合',
+        value: '',
+        is_show: 1
+      },
       {
         name: '求和',
         value: 'sum',
@@ -417,7 +434,8 @@ const currentConfig = ref({
   sort_type: 'desc',
   format_type: 'auto',
   top_n: 3,
-  aggregation_type: 'sum'
+  aggregation_type: 'sum',
+  yoy_or_mom: ''
 });
 
 const openSubMenuKey = ref(null);
@@ -488,6 +506,11 @@ function handleChildClick(child, parent) {
   emit('action', { parent, child });
   closeMenu();
 }
+
+function handleCopy() {
+  emit('action', 'copy');
+  closeMenu();
+}
 // 🔄 监听 visible 变化:当父组件隐藏菜单时,也清空子菜单状态
 watch(
   () => props.visible,
@@ -504,9 +527,27 @@ watch(
   () => props.actions,
   (actions) => {
     actionMenuItems.value = { ...actions };
+
+    // 同环比:兜底注入“无(不配置)”选项,确保既可配置也可不配置
+    if (actionMenuItems.value?.yoy_or_mom_config?.children) {
+      const children = actionMenuItems.value.yoy_or_mom_config.children;
+      const hasNone = children.some((it) => it && it.value === '');
+      if (!hasNone) {
+        actionMenuItems.value.yoy_or_mom_config.children = [
+          { name: '无', value: '', is_show: 1 },
+          ...children
+        ];
+      }
+    }
+
     actionMenuItems.value.aggregation_config = {
       is_show: 1,
       children: [
+        {
+          name: '无聚合',
+          value: '',
+          is_show: 1
+        },
         {
           name: '求和',
           value: 'sum',
@@ -542,7 +583,14 @@ watch(
 watch(
   () => props.actionConfig,
   (newVal) => {
-    currentConfig.value = { ...newVal };
+    currentConfig.value = {
+      sort_type: 'desc',
+      format_type: 'auto',
+      top_n: 3,
+      aggregation_type: 'sum',
+      yoy_or_mom: '',
+      ...newVal
+    };
   },
   {
     deep: true
@@ -565,6 +613,12 @@ watch(
   box-sizing: border-box;
 }
 
+.menu-divider {
+  height: 1px;
+  background: #eee;
+  margin: 6px 0;
+}
+
 /* 每个菜单项 */
 .menu-item {
   position: relative;
@@ -579,12 +633,29 @@ watch(
   box-sizing: border-box;
 }
 
+.menu-item > span {
+  flex: 1;
+  min-width: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
 .menu-item:hover {
   background-color: #f0f0f0;
 }
 
+.menu-item.plain {
+  border: none;
+  width: 100%;
+  font-size: 14px;
+  text-align: left;
+  background: transparent;
+}
+
 /* 子菜单箭头 */
 .submenu-arrow {
+  flex: 0 0 auto;
   margin-left: 6px;
 }
 
@@ -601,6 +672,7 @@ watch(
   min-width: 120px;
   padding: 8px 2px;
   z-index: 1002;
+  box-sizing: border-box;
 }
 
 /* 向上显示的二级子菜单 */
@@ -618,6 +690,7 @@ watch(
   text-align: left;
   cursor: pointer;
   font-size: 14px;
+  box-sizing: border-box;
 }
 
 .submenu-item:hover {
@@ -636,6 +709,15 @@ watch(
   justify-content: space-between;
   padding: 6px 12px;
   cursor: pointer;
+  box-sizing: border-box;
+}
+
+.submenu-item.has-children > span {
+  flex: 1;
+  min-width: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
 
 .submenu-item.has-children:hover {

+ 15 - 3
src/modules/report-template/filter-model/MentionFilter.vue

@@ -10,8 +10,8 @@
           <a-popover>
             <div class="measure-item">
               <img :src="MetricsIcon" alt="" />
-              <span>{{ measure_name }}</span>
-              <span v-if="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>
             <template #content>
@@ -135,7 +135,7 @@ const measure_name = computed(() => {
   return props.filter_option.option.field_name;
 });
 const measure_aggregate = computed(() => {
-  return aggregation_map[reportEditor.QueryTargetAggregationType] || '求和';
+  return aggregation_map[reportEditor.QueryTargetAggregationType] || '无聚合';
 });
 
 const dimension_list = ref([]);
@@ -514,6 +514,18 @@ defineExpose({
   border: 1px solid #0a817e;
   border-radius: 6px;
   cursor: pointer;
+  max-width: 100%;
+}
+.measure-name {
+  flex: 1 1 auto;
+  min-width: 0; /* allow flex item to shrink for ellipsis */
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.measure-aggregate {
+  flex: 0 0 auto;
+  white-space: nowrap;
 }
 .item-end-icon {
   position: absolute;

+ 30 - 11
src/modules/report-template/filter-model/model/DateModel.vue

@@ -37,12 +37,21 @@
 
       <!-- 日期选择 -->
       <a-form-item v-if="formModel.rangeType === 'start'" label="过滤条件">
+        <a-month-picker
+          v-if="props.filter?.format_str === 'year_month'"
+          v-model="formModel.startDate"
+          :disabled-date="disabledDate"
+          :show-now-btn="false"
+          class="date-picker-custom"
+          style="width: 100%"
+        />
         <a-date-picker
+          v-else
           v-model="formModel.startDate"
-          :showTime="props.filter?.format_str === 'datetime'"
+          :show-time="props.filter?.format_str === 'datetime'"
           :type="props.filter?.format_str === 'datetime' ? 'datetime' : 'date'"
-          :disabledDate="disabledDate"
-          :disabledTime="props.filter?.format_str === 'datetime' ? disabledTime : undefined"
+          :disabled-date="disabledDate"
+          :disabled-time="props.filter?.format_str === 'datetime' ? disabledTime : undefined"
           :show-now-btn="false"
           class="date-picker-custom"
           style="width: 100%"
@@ -50,12 +59,21 @@
       </a-form-item>
 
       <a-form-item v-if="formModel.rangeType === 'end'" label="过滤条件">
+        <a-month-picker
+          v-if="props.filter?.format_str === 'year_month'"
+          v-model="formModel.endDate"
+          :disabled-date="disabledDate"
+          :show-now-btn="false"
+          class="date-picker-custom"
+          style="width: 100%"
+        />
         <a-date-picker
+          v-else
           v-model="formModel.endDate"
-          :showTime="props.filter?.format_str === 'datetime'"
+          :show-time="props.filter?.format_str === 'datetime'"
           :type="props.filter?.format_str === 'datetime' ? 'datetime' : 'date'"
-          :disabledDate="disabledDate"
-          :disabledTime="props.filter?.format_str === 'datetime' ? disabledTime : undefined"
+          :disabled-date="disabledDate"
+          :disabled-time="props.filter?.format_str === 'datetime' ? disabledTime : undefined"
           :show-now-btn="false"
           class="date-picker-custom"
           style="width: 100%"
@@ -65,21 +83,22 @@
       <a-form-item v-if="formModel.rangeType === 'between'" label="过滤条件">
         <a-range-picker
           v-model="formModel.dateRange"
-          :showTime="props.filter?.format_str === 'datetime'"
+          :show-time="props.filter?.format_str === 'datetime'"
           :type="props.filter?.format_str === 'datetime' ? 'datetime' : 'date'"
-          :disabledDate="disabledDate"
-          :disabledTime="props.filter?.format_str === 'datetime' ? disabledRangeTime : undefined"
-          :timePickerProps="{ hideDisabledOptions: true }"
+          :disabled-date="disabledDate"
+          :disabled-time="props.filter?.format_str === 'datetime' ? disabledRangeTime : undefined"
+          :time-picker-props="{ hideDisabledOptions: true }"
           :show-now-btn="false"
           class="date-picker-custom"
           style="width: 100%"
+          :mode="props.filter?.format_str === 'year_month' ? 'month' : ''"
         />
       </a-form-item>
     </a-form>
   </a-modal>
 </template>
 
-<script setup >
+<script setup>
 import { reactive, ref, computed, watch, nextTick, onMounted } from 'vue';
 import DimensionDate from '../../../../assets/report-template/diemension_date.svg';
 

+ 1 - 2
src/views/Agent/AgentCentric.vue

@@ -9,7 +9,7 @@
       <CategoryTabs></CategoryTabs>
     </div> -->
     <div class="agent-list">
-      <AgentList v-if="show_agent_list" :auth="auth" @toConversation="toConversation"></AgentList>
+      <AgentList v-if="show_agent_list" :auth="auth" @to-conversation="toConversation" />
     </div>
 
     <img class="bg-left-footer" src="../../assets/bg/bg-left-footer.svg" alt="" />
@@ -42,7 +42,6 @@ const toConversation = () => {
 const getAuthFun = async () => {
   try {
     const res = await getAuth(user_id.value);
-    console.log(res, 789);
     if (res.code / 1 === 200) {
       auth.value = {
         znwd: res.data.AI_ZNWD || 0,