Prechádzať zdrojové kódy

✨ feat<报表管理>:

普通展示、合并展示表格增加排序;
任务列表添加下载接口;
xinyan 10 mesiacov pred
rodič
commit
df88d7e186

+ 9 - 0
src/views/reportManage/TaskManage/api.ts

@@ -57,3 +57,12 @@ export function getCurrencyCodeSelect(query) {
     params: query,
   });
 }
+
+export function exportTaskData(query) {
+  return request({
+    url: '/api/report_manage/summary-tasks/download/',
+    method: 'GET',
+    params: query,
+    responseType: 'blob'
+  });
+}

+ 126 - 16
src/views/reportManage/TaskManage/index.vue

@@ -8,7 +8,7 @@ import {
   getOperationSelect,
   postUpdateTask,
   postDeleteTask,
-  getCurrencyCodeSelect
+  getCurrencyCodeSelect, exportTaskData
 } from '/src/views/reportManage/TaskManage/api.ts';
 import { ComponentSize, ElMessage, FormInstance, FormRules } from 'element-plus';
 import { Plus, Delete } from '@element-plus/icons-vue';
@@ -24,6 +24,10 @@ interface taskRuleForm {
   operation: string[];
   currency: string;
   currencyCodePlatform: string;
+  line: string;
+  ipaddress: string;
+  company: string;
+  platform: string;
 }
 
 const formSize = ref<ComponentSize>('default');
@@ -50,6 +54,10 @@ const rules = reactive<FormRules>({
   operation: [{required: true, message: '请选择运营', trigger: 'change'}],
   currency: [{required: true, message: '请输入回款/余额币种', trigger: 'blur'}],
   currencyCodePlatform: [{required: true, message: '请输入平台货币', trigger: 'blur'}],
+  line: [{required: true, message: '请输入线路', trigger: 'blur'}],
+  ipaddress: [{required: true, message: '请输入IP地址', trigger: 'blur'}],
+  company: [{required: true, message: '请输入注册公司', trigger: 'blur'}],
+  platform: [{required: true, message: '请输入平台', trigger: 'blur'}],
 });
 const currencyList = ref([]);
 
@@ -64,6 +72,10 @@ interface RowVO {
   currencyCodePlatform: string;
   child_user_number: number;
   user: [];
+  line: string;
+  ipaddress: string;
+  company: string;
+  platform: string;
 }
 
 const xGrid = ref<VxeGridInstance<RowVO>>();
@@ -73,8 +85,8 @@ let allTasks = []; // 用于存储所有任务数据
 const gridOptions = reactive<VxeGridProps<RowVO>>({
   border: 'inner',
   keepSource: true,
-  showOverflow: true,
-  height: 850,
+  //showOverflow: true,
+  height: 900,
   loading: false,
   round: true,
   toolbarConfig: {
@@ -84,7 +96,8 @@ const gridOptions = reactive<VxeGridProps<RowVO>>({
     },
     slots: {
       buttons: 'toolbar_buttons',
-    },
+      tools: 'toolbar_tools'
+    }
   },
   rowConfig: {
     isHover: true,
@@ -117,19 +130,39 @@ const gridOptions = reactive<VxeGridProps<RowVO>>({
       title: '平台编号',
       editRender: {autofocus: '.vxe-input--inner'},
       slots: {edit: 'number_edit'},
-      sortable: true,
     },
     {
       field: 'platformName',
       title: '平台名称',
       editRender: {autofocus: '.vxe-input--inner'},
-      slots: {edit: 'name_edit'}
+      slots: {edit: 'name_edit'},
+      align: 'center',
+      width: 150
+    },
+    {
+      field: 'country',
+      title: '国家',
+      editRender: {autofocus: '.vxe-input--inner'},
+      slots: {edit: 'country_edit'},
+      width: 89,
+      align: 'center'
+    },
+    {field: 'brandName', title: '品牌', editRender: {}, slots: {edit: 'brand_edit'}, align: 'center'},
+    {field: 'user_name', title: '运营', editRender: {}, slots: {edit: 'operation_edit'}, align: 'center'},
+    {field: 'currencyCode', title: '回款币种', editRender: {}, slots: {edit: 'currency_edit'}, align: 'center'},
+    {
+      field: 'currencyCodePlatform',
+      title: '回款/余额币种',
+      editRender: {},
+      slots: {edit: 'currencyCodePlatform_edit'},
+      width: 130,
+      align: 'center'
     },
-    {field: 'country', title: '国家', editRender: {autofocus: '.vxe-input--inner'}, slots: {edit: 'country_edit'}},
-    {field: 'brandName', title: '品牌', editRender: {}, slots: {edit: 'brand_edit'}},
-    {field: 'user_name', title: '运营', editRender: {}, slots: {edit: 'operation_edit'}},
-    {field: 'currencyCode', title: '回款币种', editRender: {}, slots: {edit: 'currency_edit'}},
-    {field: 'currencyCodePlatform', title: '回款/余额币种', editRender: {}, slots: {edit: 'currencyCodePlatform_edit'}},
+    {field: 'line', title: '线路', editRender: {}, slots: {edit: 'line_edit'}, align: 'center'},
+    {field: 'ipaddress', title: 'IP地址', editRender: {}, slots: {edit: 'ipaddress_edit'}, width: 138},
+    {field: 'company', title: '注册公司', editRender: {}, slots: {edit: 'company_edit'}, align: 'center'},
+    {field: 'platform', title: '平台', editRender: {}, slots: {edit: 'platform_edit'}, align: 'center'},
+    {field: 'status', title: '状态', editRender: {}, slots: {edit: 'status_edit'}, align: 'center'},
     {title: '操作', width: 120, slots: {default: 'operate'}},
   ],
   data: [],
@@ -251,6 +284,11 @@ async function deleteTask() {
   }
 }
 
+const isDeleteDisabled = computed(() => {
+  const $grid = xGrid.value;
+  return !$grid || $grid.getCheckboxRecords().length === 0;
+});
+
 const removeEvent = async () => {
   const $grid = xGrid.value;
   if ($grid) {
@@ -274,7 +312,12 @@ const requiredFields = [
   {field: 'brandName', title: '品牌'},
   {field: 'user', title: '运营'},
   {field: 'currencyCode', title: '回款币种'},
-  {field: 'currencyCodePlatform', title: '回款/余额币种'}
+  {field: 'currencyCodePlatform', title: '回款/余额币种'},
+  {field: 'line', title: '线路'},
+  {field: 'ipaddress', title: 'IP地址'},
+  {field: 'company', title: '注册公司'},
+  {field: 'platform', title: '平台'},
+  {field: 'status', title: '状态'},
 ];
 
 const validateRow = (row) => {
@@ -307,6 +350,11 @@ async function updateRow(row) {
       user: row.user,
       currencyCode: row.currencyCode,
       currencyCodePlatform: row.currencyCodePlatform,
+      line: row.line,
+      ipaddress: row.ipaddress,
+      company: row.company,
+      platform: row.platform,
+      status: row.status,
     };
     //console.log('updatedRowData', updatedRowData);
     try {
@@ -348,6 +396,10 @@ async function createTask() {
     brandName: taskRuleForm.brand,
     currencyCode: taskRuleForm.currency,
     currencyCodePlatform: taskRuleForm.currencyCodePlatform,
+    line: taskRuleForm.line,
+    ipaddress: taskRuleForm.ipaddress,
+    company: taskRuleForm.company,
+    platform: taskRuleForm.platform,
     user: taskRuleForm.operation,
   };
   try {
@@ -389,7 +441,18 @@ const submitForm = async (formEl) => {
   });
 };
 
-
+async function handleExport() {
+  gridOptions.loading = true;
+  const response = await exportTaskData();
+  const url = window.URL.createObjectURL(new Blob([response.data]));
+  const link = document.createElement('a');
+  link.href = url;
+  link.setAttribute('download', '店铺数据.xlsx');
+  document.body.appendChild(link);
+  link.click();
+  gridOptions.loading = false;
+  ElMessage.success('导出数据成功');
+}
 
 function handleClose(done: Function) {
   if (taskRuleFormRef.value) taskRuleFormRef.value.resetFields();
@@ -431,7 +494,7 @@ const handleCurrencyCodePlatformSelect = item => {
   taskRuleForm.currencyCodePlatform = item;
 };
 
-function handleRowSelect (item, row){
+function handleRowSelect(item, row) {
   row.currencyCode = item;
 }
 
@@ -439,6 +502,19 @@ function handelRowCurrencyCodePlatformSelect(item, row) {
   row.currencyCodePlatform = item;
 }
 
+const cellStyle = () => {
+  return {
+    fontSize: '13px',
+    fontWeight: '500',
+  };
+};
+
+const headerCellStyle = () => {
+  return {
+    fontSize: '14px',
+  };
+};
+
 
 onMounted(() => {
   getTaskList();
@@ -453,10 +529,17 @@ onMounted(() => {
   </el-card>
   <el-card class="mx-8 my-3">
     <div style="position: relative">
-      <vxe-grid ref="xGrid" stripe v-bind="gridOptions" v-on="gridEvents">
+      <vxe-grid ref="xGrid" :cell-style="cellStyle" :header-cell-style="headerCellStyle" stripe v-bind="gridOptions"
+                v-on="gridEvents">
         <template #toolbar_buttons>
           <el-button :icon="Plus" plain type="primary" @click="dialogFormVisible = true"> 添加任务</el-button>
-          <el-button :icon="Delete" plain type="danger" @click="removeEvent">删除</el-button>
+          <el-button :disabled="isDeleteDisabled" :icon="Delete" plain type="danger" @click="removeEvent">删除
+          </el-button>
+        </template>
+        <template #toolbar_tools>
+          <div class="mx-3.5">
+            <vxe-button circle icon="vxe-icon-download" @click="handleExport"></vxe-button>
+          </div>
         </template>
         <template #operate="{ row }">
           <template v-if="hasActiveEditRow(row)">
@@ -479,6 +562,21 @@ onMounted(() => {
         <template #brand_edit="{ row }">
           <vxe-input v-model="row.brandName"></vxe-input>
         </template>
+        <template #line_edit="{ row }">
+          <vxe-input v-model="row.line"></vxe-input>
+        </template>
+        <template #ipaddress_edit="{ row }">
+          <vxe-input v-model="row.ipaddress"></vxe-input>
+        </template>
+        <template #company_edit="{ row }">
+          <vxe-input v-model="row.company"></vxe-input>
+        </template>
+        <template #platform_edit="{ row }">
+          <vxe-input v-model="row.platform"></vxe-input>
+        </template>
+        <template #status_edit="{ row }">
+          <vxe-input v-model="row.status"></vxe-input>
+        </template>
         <template #operation_edit="{ row }">
           <vxe-select v-model="row.user" multiple>
             <vxe-option v-for="item in operationList" :key="item.value" :label="item.label"
@@ -577,6 +675,18 @@ onMounted(() => {
           </template>
         </el-autocomplete>
       </el-form-item>
+      <el-form-item label="线路" prop="line">
+        <el-input v-model="taskRuleForm.line" placeholder="请输入线路" />
+      </el-form-item>
+      <el-form-item label="IP地址" prop="ipaddress">
+        <el-input v-model="taskRuleForm.ipaddress" placeholder="请输入IP地址" />
+      </el-form-item>
+      <el-form-item label="注册公司" prop="company">
+        <el-input v-model="taskRuleForm.company" placeholder="请输入" />
+      </el-form-item>
+      <el-form-item label="平台" prop="platform">
+        <el-input v-model="taskRuleForm.platform" placeholder="请输入" />
+      </el-form-item>
     </el-form>
     <template #footer>
       <div class="dialog-footer">

+ 51 - 4
src/views/reportManage/dataCenter/combinedDisplay/components/tableData/mainData.vue

@@ -21,6 +21,11 @@ const weekEnd = ref(null);
 const monthStartDate = ref(null);
 const monthEndDate = ref(null);
 
+//排序
+const order_date = ref('');
+const sortOrder = ref('');
+const dateType = ref('');
+
 const gridOptions = reactive({
   border: 'inner',
   height: 900,
@@ -40,6 +45,9 @@ const gridOptions = reactive({
     pageSize: 20,
     pageSizes: [10, 20, 30],
   },
+  sortConfig: {
+    remote: true
+  },
   toolbarConfig: {
     custom: true,
     zoom: {
@@ -82,6 +90,9 @@ async function fetchMainData(taskIds, resetPage = false) {
       week_end_date: weekEnd.value,
       month_start_date: monthStartDate.value,
       month_end_date: monthEndDate.value,
+      sort: sortOrder.value,
+      order_date: order_date.value,
+      date_type: dateType.value,
     });
     gridOptions.data = response.data;
     gridOptions.pagerConfig.total = response.total;
@@ -107,12 +118,17 @@ async function fetchMainData(taskIds, resetPage = false) {
       const lastColumns = [];
 
       allColumns.forEach(key => {
+        let isSortable = false;
+        if (key.includes('销售额') && !key.includes('广告销售额') && !key.includes('增长率')) {
+          isSortable = true;
+        }
         const column = {
           field: key,
           title: key, // 使用字段名作为列标题
-          minWidth: key.includes('~') ? 90 : key.includes('截止')? 90 :regex2.test(key) ? 81 : 70,
+          minWidth: key.includes('~') ? 90 : key.includes('截止')? 90 :regex2.test(key) ? 83 : 74,
           align: 'center',
           formatter: formatEmptyCell,
+          sortable: isSortable,
         };
         if (middleKeywords.some(kw => key.includes(kw))) {
           middleColumns.push(column);
@@ -141,6 +157,27 @@ async function fetchMainData(taskIds, resetPage = false) {
   }
 }
 
+function handleSortChange({ column, order }) {
+  sortOrder.value = order === 'asc' ? 'smallfirst' : 'bigfirst';
+  const sortField = column.field;
+  if (sortField) {
+    const match = sortField.match(/(\d{4}-\d{2}-\d{2})的销售额/);
+    const matchRange = sortField.match(/(\d{4}-\d{2}-\d{2})~(\d{4}-\d{2}-\d{2})的销售额/);
+    const matchMonth = sortField.match(/(\d{4}-\d{2})的销售额/);
+    if(matchRange){
+      order_date.value = matchRange[1];
+      dateType.value = 'week';
+    } else if (match) {
+      order_date.value = match[1];
+      dateType.value = 'day';
+    }else if (matchMonth) {
+      order_date.value = `${matchMonth[1]}-01`;
+      dateType.value = 'month';
+    }
+  }
+  fetchMainData(props.taskIds,true);
+}
+
 // 监测 taskIds 变化
 watch(() => props.taskIds, (newTaskIds) => {
   fetchMainData(newTaskIds, true)
@@ -228,14 +265,24 @@ async function handleExport() {
   }
 }
 
-function formatEmptyCell({cellValue}) {
-  return cellValue === null || cellValue === undefined || cellValue === '' ? '--' : cellValue;
+function formatEmptyCell({ cellValue }) {
+  if (cellValue === null || cellValue === undefined || cellValue === '') {
+    return '--';
+  }
+  if (typeof cellValue === 'number') {
+    const formatter = new Intl.NumberFormat('en-US', {
+      minimumFractionDigits: 2,
+      maximumFractionDigits: 2,
+    });
+    return formatter.format(cellValue);
+  }
+  return cellValue;
 }
 </script>
 
 <template>
   <vxe-grid :cell-style="cellStyle" :header-cell-style="cellStyleHandler" v-bind="gridOptions"
-            v-on="gridEvents">
+            v-on="gridEvents" @sort-change="handleSortChange">
     <template #toolbar_buttons>
       <div class="mx-3.5">
         <vxe-button circle icon="vxe-icon-download" @click="handleExport"></vxe-button>

+ 11 - 1
src/views/reportManage/dataCenter/combinedDisplay/components/tableData/monthlyComparativeData.vue

@@ -119,7 +119,17 @@ watch(() => props.taskIds, (newTaskIds) => {
 });
 
 function formatEmptyCell({ cellValue }) {
-  return cellValue === null || cellValue === undefined || cellValue === '' ? '--' : cellValue;
+  if (cellValue === null || cellValue === undefined || cellValue === '') {
+    return '--';
+  }
+  if (typeof cellValue === 'number') {
+    const formatter = new Intl.NumberFormat('en-US', {
+      minimumFractionDigits: 2,
+      maximumFractionDigits: 2,
+    });
+    return formatter.format(cellValue);
+  }
+  return cellValue;
 }
 
 const cellStyle =() => {

+ 52 - 8
src/views/reportManage/dataCenter/normalDisplay/components/TableDataDisplay.vue

@@ -1,5 +1,5 @@
 <script lang="ts" setup>
-import { ref, reactive, computed, inject, watch, Ref, onMounted, provide } from 'vue';
+import { ref, reactive, computed, inject, watch, Ref, onMounted, defineProps } from 'vue';
 import { useRouter } from 'vue-router';
 import { getDayData, getMonthData, getWeekData } from '/src/views/reportManage/dataCenter/api';
 import { dayDataColumns, weekDataColumns, monthDataColumns } from '../../utils/columns';
@@ -24,6 +24,10 @@ const dayData = [];
 const weekData = [];
 const monthData = [];
 
+//排序
+const order_date = ref('');
+const sortOrder = ref('');
+
 const gridOptions = reactive({
   border: 'inner',
   height: 900,
@@ -45,6 +49,9 @@ const gridOptions = reactive({
     pageSize: 20,
     pageSizes: [10, 20, 30],
   },
+  sortConfig: {
+    remote: true
+  },
   toolbarConfig: {
     custom: true,
     zoom: {
@@ -99,6 +106,8 @@ watch(currentDate, (newValue) => {
     monthStartDate.value = dayjs(newValue.startDate).format('YYYY-MM-DD');
     monthEndDate.value = dayjs(newValue.endDate).format('YYYY-MM-DD');
   }
+  sortOrder.value = '';
+  order_date.value = '';
   fetchCurrentData(props.taskIds);
 });
 
@@ -111,6 +120,8 @@ async function fetchData(taskIds, apiFunc, startDate, endDate, dataColumns, date
       [`${dateTypeKey}_start_date`]: startDate.value,
       [`${dateTypeKey}_end_date`]: endDate.value,
       task_ids: taskIds,
+      sort: sortOrder.value,
+      order_date: order_date.value,
     });
     gridOptions[dateType.value].data = resp.data;
     gridOptions.pagerConfig.total = resp.total;
@@ -127,12 +138,17 @@ async function fetchData(taskIds, apiFunc, startDate, endDate, dataColumns, date
       });
       // 将所有可能的列添加到 dynamicColumns
       allColumns.forEach(key => {
+        let isSortable = false;
+        if (key.includes('销售额') && !key.includes('广告销售额') && !key.includes('增长率')) {
+          isSortable = true;
+        }
         dynamicColumns.push({
           field: key,
           title: key,
-          minWidth: key.includes('~') ? 101 : /\d{4}-\d{2}-\d{2}的/.test(key) ? 88 : 78,
+          minWidth: key.includes('~') ? 95 : /\d{4}-\d{2}-\d{2}的/.test(key) ? 88 : 82,
           align: 'center',
           formatter: formatEmptyCell,
+          sortable: isSortable,
         });
       });
       if (dateType.value === dateTypeKey) {
@@ -161,7 +177,7 @@ async function fetchMonthData(taskIds) {
   await fetchData(taskIds, getMonthData, monthStartDate, monthEndDate, monthDataColumns, 'month');
 }
 
-function fetchCurrentData(taskIds,resetPage = false) {
+function fetchCurrentData(taskIds, resetPage = false) {
   if (resetPage) {
     gridOptions.pagerConfig.currentPage = 1; // 重置页码为第一页
   }
@@ -174,6 +190,24 @@ function fetchCurrentData(taskIds,resetPage = false) {
   }
 }
 
+function handleSortChange({ column, order }) {
+  sortOrder.value = order === 'asc' ? 'smallfirst' : 'bigfirst';
+  const sortField = column.field;
+  if (sortField) {
+    const match = sortField.match(/(\d{4}-\d{2}-\d{2})的销售额/);
+    const matchRange = sortField.match(/(\d{4}-\d{2}-\d{2})~(\d{4}-\d{2}-\d{2})的销售额/);
+    const matchMonth = sortField.match(/(\d{4}-\d{2})的销售额/);
+    if(matchRange){
+      order_date.value = matchRange[1];
+    } else if (match) {
+      order_date.value = match[1];
+    }else if (matchMonth) {
+      order_date.value = `${matchMonth[1]}-01`;
+    }
+  }
+  fetchCurrentData(props.taskIds,true);
+}
+
 watch(() => props.taskIds, (newTaskIds) => {
   fetchCurrentData(newTaskIds, true); // 添加第二个参数表示重置页码
 });
@@ -188,17 +222,27 @@ const handleImport = () => {
 };
 
 function formatEmptyCell({ cellValue }) {
-  return cellValue === null || cellValue === undefined || cellValue === '' ? '--' : cellValue;
+  if (cellValue === null || cellValue === undefined || cellValue === '') {
+    return '--';
+  }
+  if (typeof cellValue === 'number') {
+    const formatter = new Intl.NumberFormat('en-US', {
+      minimumFractionDigits: 2,
+      maximumFractionDigits: 2,
+    });
+    return formatter.format(cellValue);
+  }
+  return cellValue;
 }
 
-const cellStyle =() => {
+const cellStyle = () => {
   return {
     fontSize: '12px',
     fontWeight: '500',
   };
 }
 
-const headerCellStyle =() => {
+const headerCellStyle = () => {
   return {
     fontSize: '12px',
   };
@@ -211,12 +255,12 @@ onMounted(() => {
 
 <template>
   <div>
-    <vxe-grid :header-cell-style="headerCellStyle" :cell-style="cellStyle"  v-bind="currentGridOptions" v-on="gridEvents" stripe>
+    <vxe-grid :header-cell-style="headerCellStyle" :cell-style="cellStyle" v-bind="currentGridOptions" v-on="gridEvents" stripe @sort-change="handleSortChange">
       <template #toolbar_buttons>
         <vxe-button icon="vxe-icon-add" status="primary" @click="handleImport">数据录入</vxe-button>
       </template>
       <template #platformNumber_default="{ row }">
-        <div class="font-semibold">{{ row.platformNumber}}</div>
+        <div class="font-semibold">{{ row.platformNumber }}</div>
       </template>
       <template #platformName_default="{ row }">
         <div class="font-semibold" style="color: #0097f8">{{ row.platformName}}</div>

+ 1 - 1
src/views/reportManage/dataCenter/normalDisplay/components/TableDataEntry.vue

@@ -224,7 +224,7 @@ const originalDataMap = new Map();
 
 const gridOptions = reactive({
   border: 'inner',
-  height: 800,
+  height: 900,
   align: null,
   round: true,
   loading: false,