zuy 2 年之前
父节点
当前提交
6d2c739fd2

+ 42 - 25
admin-web/src/components/form/ExtSelect.vue

@@ -16,13 +16,13 @@
     <template #prefix>
       <SvgIcon :name="prefix"/>
     </template>
-<!--    <el-option :key="item.id" v-for="item in state.list" :value="item.id" :label="item.name||item.title"> {{ item.name || item.title }}</el-option>-->
+    <!--    <el-option :key="item.id" v-for="item in state.list" :value="item.id" :label="item.name||item.title"> {{ item.name || item.title }}</el-option>-->
   </el-select-v2>
 </template>
 <script lang="ts" setup name="ExtSelect">
 import {onMounted, ref, nextTick, watch, reactive} from 'vue';
 import u from "/@/utils/u";
-import {$body, $post,$get} from "/@/utils/request";
+import {$body, $post, $get} from "/@/utils/request";
 
 const emit = defineEmits(['update:modelValue', 'on-change']);
 
@@ -62,29 +62,43 @@ const props = defineProps({
   dataList: {
     type: Array<any>
   },
-  urlMethod:{
-    type:String,
-    default:'post'
+  urlMethod: {
+    type: String,
+    default: 'post'
   },
-  labelKey:{
-    type:String,
-    default:'name'
+  labelKey: {
+    type: String,
+    default: 'name'
   },
- valueKey:{
-    type:String,
-    default:'id'
+  valueKey: {
+    type: String,
+    default: 'id'
   }
 })
 
 const state = reactive({
-  list: [] ,
+  list: [] as Array<any>,
   modelVal: null
 })
 
-watch(()=>props.modelValue,(val)=>{
-  console.log("watch>>>",val)
+watch(() => props.modelValue, (val) => {
+  console.log("watch>>>", val)
   state.modelVal = val;
+})
+
+
+watch(() => props.dataList, (val) => {
+  console.log("watch>>>", val)
+  nextTick(() => {
+    if (!u.isEmptyOrNull(props.dataList)) {
+      // state.list = props.dataList;
+      state.list = props.dataList?.map((k: any) => {
+        return {value: k[props.valueKey], label: k[props.labelKey]}
+      })
+    }
   })
+},{deep:true})
+
 
 onMounted(() => {
   loadData();
@@ -92,7 +106,10 @@ onMounted(() => {
 
 const loadData = () => {
   if (!u.isEmptyOrNull(props.dataList)) {
-    state.list = props.dataList;
+    // state.list = props.dataList;
+    state.list = props.dataList?.map((k: any) => {
+      return {value: k[props.valueKey], label: k[props.labelKey]}
+    })
   } else {
     if (!props.url) {
       return;
@@ -104,25 +121,25 @@ const loadData = () => {
     if (props.query) {
       query = Object.assign({}, query, props.query);
     }
-    if(props.urlMethod?.toLowerCase()==='post'){
+    if (props.urlMethod?.toLowerCase() === 'post') {
       $body(`${props.url}`, query).then((list: any) => {
         // let {list,count}  = res;
-        state.list =  list.map((k:any)=>{
-          return {value:k[props.valueKey],label:k[props.labelKey]}
+        state.list = list.map((k: any) => {
+          return {value: k[props.valueKey], label: k[props.labelKey]}
         })
         console.log(state.list)
         // state.value.list = res?.list
       });
-    }else{
+    } else {
       $get(`${props.url}`, query).then((list: any) => {
         // let {list,count}  = res;
-        if(list.list){
-          state.list =  list.list.map((k:any)=>{
-            return {value:k[props.valueKey],label:k[props.labelKey]}
+        if (list.list) {
+          state.list = list.list.map((k: any) => {
+            return {value: k[props.valueKey], label: k[props.labelKey]}
           })
-        }else{
-          state.list =  list.map((k:any)=>{
-            return {value:k[props.valueKey],label:k[props.labelKey]}
+        } else {
+          state.list = list.map((k: any) => {
+            return {value: k[props.valueKey], label: k[props.labelKey]}
           })
         }
 

+ 352 - 88
admin-web/src/views/admin/kanban/index.vue

@@ -25,125 +25,196 @@
 }
 </style>
 <template>
-  <div class="system-container layout-padding">
-    <el-card shadow="hover" class="layout-padding-auto">
-
-      <el-form
-          :model="state.formQuery"
-          ref="queryRef"
-          size="default" label-width="0px" class="mt5 mb5">
+  <div class="system-container1 layout-padding">
+
+    <el-form
+        inline
+        :model="state.formQuery"
+        ref="queryRef"
+        size="default" label-width="0px" class="mt5 mb5">
+      <el-form-item>
         <ext-select
             v-model="state.formQuery.stationIdList"
-            placeholder="活动名称"
+            placeholder="站点"
+            value-key="stationId"
+            label-key="stationName"
             clearable
             multiple
             :data-list="state.stationList"
-            @blur="loadData(true)"
-            class="wd150 mr10">
+            style="min-width: 150px;"
+            class=" mr10">
         </ext-select>
-
+      </el-form-item>
+
+      <el-form-item>
+        <el-radio-group v-model="state.formQuery.type" size="default" @change="handleQueryTypeChange">
+          <el-radio-button label="day">天</el-radio-button>
+          <el-radio-button label="month">月</el-radio-button>
+        </el-radio-group>
+        <el-date-picker
+            :value-format="state.dateFormat"
+            v-model="state.formQuery.dateRange"
+            :type="state.dateType"
+            unlink-panels
+            range-separator="至"
+            start-placeholder="开始时间"
+            end-placeholder="结束时间"
+            :shortcuts="shortcuts"
+        />
+      </el-form-item>
+
+      <el-form-item>
         <el-button class="ml10" plain size="default" type="success" @click="loadData()">
           <SvgIcon name="ele-Search"/>
           查询
         </el-button>
-      </el-form>
-
-    </el-card>
-
-    <el-card>
-      <el-card shadow="hover" class="layout-padding-auto">
-        <template #header>充电用户数</template>
-        <template #default>
-          XXXx
-        </template>
-      </el-card>
-
-      <el-card shadow="hover" class="layout-padding-auto">
-        <template #header>有效订单数</template>
-        <template #default>
-          XXXx
-        </template>
-      </el-card>
-
-      <el-card shadow="hover" class="layout-padding-auto">
-        <template #header>总电量</template>
-        <template #default>
-          XXXx
-        </template>
-      </el-card>
-
-
-      <el-card shadow="hover" class="layout-padding-auto">
-        <template #header>总金额</template>
-        <template #default>
-          XXXx
-        </template>
-      </el-card>
-
-
-      <el-card shadow="hover" class="layout-padding-auto">
-        <template #header>总电费</template>
-        <template #default>
-          XXXx
-        </template>
+      </el-form-item>
+    </el-form>
+
+    <el-scrollbar>
+      <el-card>
+        <el-card shadow="hover" class="layout-padding-auto">
+          <template #header>充电用户数</template>
+          <template #default>
+            <div style="min-height: 500px;padding: 20px;" ref="chartOneRef"></div>
+          </template>
+        </el-card>
+
+        <el-card shadow="hover" class="layout-padding-auto">
+          <template #header>有效订单数</template>
+          <template #default>
+            <div style="min-height: 300px;padding: 20px;" ref="chartTwoRef"></div>
+          </template>
+        </el-card>
+
+        <el-card shadow="hover" class="layout-padding-auto">
+          <template #header>总电量</template>
+          <template #default>
+            <div style="min-height: 300px;padding: 20px;" ref="chartThreeRef"></div>
+          </template>
+        </el-card>
+
+        <!--
+                <el-card shadow="hover" class="layout-padding-auto">
+                  <template #header>总金额</template>
+                  <template #default>
+                    XXXx
+                  </template>
+                </el-card>
+
+
+                <el-card shadow="hover" class="layout-padding-auto">
+                  <template #header>总电费</template>
+                  <template #default>
+                    XXXx
+                  </template>
+                </el-card>
+
+                <el-card shadow="hover" class="layout-padding-auto">
+                  <template #header>服务费</template>
+                  <template #default>
+                    XXXx
+                  </template>
+                </el-card>
+
+                <el-card shadow="hover" class="layout-padding-auto">
+                  <template #header>平均电量</template>
+                  <template #default>
+                    XXXx
+                  </template>
+                </el-card>
+
+
+                <el-card shadow="hover" class="layout-padding-auto">
+                  <template #header>平均订单费用</template>
+                  <template #default>
+                    XXXx
+                  </template>
+                </el-card>
+
+
+                <el-card shadow="hover" class="layout-padding-auto">
+                  <template #header>平均充电用电量</template>
+                  <template #default>
+                    XXXx
+                  </template>
+                </el-card>-->
       </el-card>
 
-      <el-card shadow="hover" class="layout-padding-auto">
-        <template #header>服务费</template>
-        <template #default>
-          XXXx
-        </template>
-      </el-card>
-
-      <el-card shadow="hover" class="layout-padding-auto">
-        <template #header>平均电量</template>
-        <template #default>
-          XXXx
-        </template>
-      </el-card>
-
-
-      <el-card shadow="hover" class="layout-padding-auto">
-        <template #header>平均订单费用</template>
-        <template #default>
-          XXXx
-        </template>
-      </el-card>
-
-
-      <el-card shadow="hover" class="layout-padding-auto">
-        <template #header>平均充电用电量</template>
-        <template #default>
-          XXXx
-        </template>
-      </el-card>
-    </el-card>
+    </el-scrollbar>
   </div>
 </template>
 
-<script setup lang="ts" name="ActivityList">
-import {defineAsyncComponent, reactive, onMounted, onBeforeMount, ref, getCurrentInstance, nextTick, onBeforeUnmount} from 'vue';
+<script setup lang="ts" name="KanbanList">
+import {defineAsyncComponent, reactive, onMounted, onBeforeMount, ref, getCurrentInstance, nextTick, onUnmounted} from 'vue';
 import {$body, $get} from "/@/utils/request";
 import {Msg} from "/@/utils/message";
-
+import * as echarts from 'echarts';
 
 import mittBus from '/@/utils/mitt';
-import ExtDSelect from "/@/components/form/ExtDSelect.vue";
-import ExtDatePicker from "/@/components/form/ExtDatePicker.vue";
 import ExtSelect from "/@/components/form/ExtSelect.vue";
+import u from "/@/utils/u";
 
 //定义引用
 const queryRef = ref();
+const chartOneRef = ref();
+const chartTwoRef = ref();
+const chartThreeRef = ref();
+
+const end = new Date()
+const start = new Date()
+start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
+
 
 //定义变量
 const state = reactive({
   formQuery: {
-    stationIdList: []
+    stationIdList: [],
+    dateRange: [u.date.format(start, "YYYY-MM-DD"), u.date.format(end, "YYYY-MM-DD")],
+    type: 'day'
   },
-  stationList: []
+  stationList: [],
+  chartOne: null as any,
+  chartTwo: null as any,
+  chartThree: null as any,
+  chartData: {},
+  theme: '',
+  dateType: 'daterange',
+  dateFormat: 'YYYY-MM-DD'
 })
 
 
+const shortcuts = [
+  {
+    text: '近7天',
+    value: () => {
+      const end = new Date()
+      const start = new Date()
+      start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
+      return [start, end]
+    },
+  },
+  {
+    text: '近30天',
+    value: () => {
+      const end = new Date()
+      const start = new Date()
+      start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
+      return [start, end]
+    },
+  },
+  {
+    text: '近90天',
+    value: () => {
+      const end = new Date()
+      const start = new Date()
+      start.setTime(start.getTime() - 3600 * 1000 * 24 * 90)
+      return [start, end]
+    },
+  },
+]
+
+
 // 监听双向绑定 modelValue 的变化
 // watch(
 //         () => state.pageIndex,
@@ -161,12 +232,38 @@ onMounted(() => {
   loadData();
 });
 
+onUnmounted(() => {
+  state.chartOne?.dispose();
+  state.chartTwo?.dispose();
+  state.chartThree?.dispose();
+})
+
+window.onresize = function () {
+  //自适应大小
+  state.chartOne.resize();
+};
 
 //region 方法区
 
+const handleQueryTypeChange = (type: string) => {
+  console.log(type)
+  if (type === 'day') {
+    state.dateType = 'daterange'
+    state.dateFormat = 'YYYY-MM-DD'
+  } else {
+    state.dateType = 'monthrange'
+    state.dateFormat = 'YYYY-MM-DD'
+  }
+}
+
 const loadStationList = () => {
   $get(`/station/listStation`, {pageNum: 1024}).then((res: any) => {
     state.stationList = res;
+    state.formQuery.stationIdList = res.map(k=>k.stationId);
+    console.log(res)
+    setTimeout(()=>{
+      loadData();
+    },200)
   }).catch(e => {
     console.error(e)
   })
@@ -174,8 +271,175 @@ const loadStationList = () => {
 
 // 初始化表格数据
 const loadData = () => {
+  console.log(state.formQuery)
+  let params = {
+    stationIds: state.formQuery.stationIdList,
+    startTime: state.formQuery.dateRange[0],
+    endTime: state.formQuery.dateRange[1],
+    type: state.formQuery.type
+  }
+  for (let i = 0; i < Object.keys(params).length; i++) {
+    if (u.isEmptyOrNull(params[Object.keys(params)[i]])) {
+      return;
+    }
+  }
+  $get(`/stat/stationStatDetail`, params).then(res => {
+    console.log(res)
+    state.chartData = res;
+    buildOrderChart()
+  })
 };
 
+/**
+ * 充电人数、有限订单数
+ */
+const buildOrderChart = () => {
+  if (state.chartOne) {
+    state.chartOne.dispose();
+  }
+  state.chartOne = echarts.init(chartOneRef.value, state.theme);
+  let {type} = state.formQuery
+  let stationIdList = Object.keys(state.chartData);
+  if (u.isEmptyOrNull(stationIdList)) {
+    Msg.message('未查询到统计数据', 'error')
+    return;
+  }
+  let xAxis = state.chartData[`${stationIdList[0]}`].map((k: any) => k.statDay);
+  let legends:Array<string> =[];
+  state.stationList.filter((k: any) => stationIdList.includes(k.stationId)).forEach(item=>{
+    legends.push(item.stationName+'-充电人数')
+    legends.push(item.stationName+'-订单数')
+  })
+
+  let y1Max = 0, y2Max = 0;
+  let series: Array<any> = [];
+  stationIdList.forEach((stationId: string) => {
+    let tmpUserMax = Math.max(...state.chartData[`${stationId}`].map((k: any) => k.chargeUsers))
+    y1Max = Math.max(y1Max, tmpUserMax)
+
+    let tmpOrderMax = Math.max(...state.chartData[`${stationId}`].map((k: any) => k.validOrders))
+    y2Max = Math.max(y2Max, tmpOrderMax)
+
+    let station = state.stationList.find((k: any) => k.stationId == stationId);
+    let {stationName} = station;
+    series.push({
+      name: stationName+'-充电人数',
+      type: 'bar',
+      symbolSize: 6,
+      symbol: 'circle',
+      smooth: true,
+      itemStyle: { barBorderRadius: 5},
+      tooltip: {
+        trigger:'axis',
+        formatter: ' {a0}  {b}<br />{a} :  {c}个 '
+      },
+      data: state.chartData[stationId].map((k: any) => k.chargeUsers)
+    })
+
+
+    series.push({
+      name: stationName+'-订单数',
+      type: 'line',
+      smooth: true,
+      yAxisIndex: 1,
+      tooltip: {
+        trigger:'item',
+        formatter: '{a0}    {b}<br />{a} :  {c}个 '
+      },
+      // tooltip: {
+      //   valueFormatter: function (value) {
+      //     console.log(value)
+      //     return value + ' 人';
+      //   }
+      // },
+      data: state.chartData[stationId].map((k: any) => k.validOrders)
+    })
+
+  });
+
+  y1Max = Math.max(y1Max, 100)
+  y2Max = Math.max(y2Max, 100)
+
+  let y1Interval = Math.ceil(y1Max / 5)
+  let y2Interval =  Math.ceil(y2Max / 5)
+
+
+  let option = {
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'cross',
+        crossStyle: {
+          color: '#999'
+        }
+      }
+    },
+    toolbox: {
+      feature: {
+        // dataView: {show: true, readOnly: false},
+        // magicType: {show: true, type: ['line', 'bar']},
+        // restore: {show: true},
+        // saveAsImage: {show: true}
+      }
+    },
+    legend: {
+      // data: ['Evaporation', 'Precipitation', 'Temperature']
+      data: legends
+    },
+    xAxis: [
+      {
+        type: 'category',
+        data: xAxis,
+        // data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
+        axisPointer: {
+          type: 'shadow'
+        }
+      }
+    ],
+    yAxis: [
+      {
+        type: 'value',
+        name: '人数',
+        min: 0,
+        max: y1Max,
+        interval: y1Interval,
+        axisLabel: {
+          formatter: '{value} 人'
+        }
+      },
+      {
+        type: 'value',
+        name: '订单数',
+        min: 0,
+        max: y2Max,
+        interval: y2Interval,
+        axisLabel: {
+          formatter: '{value} 个'
+        }
+      }
+    ],
+    series: series
+  };
+  console.log(option)
+  state.chartOne.setOption(option)
+
+}
+
+
+/**
+ * 充电量
+ */
+const buildElectricChart = () => {
+
+}
+
+
+/**
+ * 充电金额
+ */
+const buildMoneyChart = () => {
+
+}
 
 //endregion
 

+ 53 - 46
admin-web/vite.config.ts

@@ -1,59 +1,66 @@
 import vue from '@vitejs/plugin-vue';
-import { resolve } from 'path';
-import { defineConfig, loadEnv, ConfigEnv } from 'vite';
+import {resolve} from 'path';
+import {defineConfig, loadEnv, ConfigEnv} from 'vite';
 // import vueSetupExtend from 'vite-plugin-vue-setup-extend';
 
 const pathResolve = (dir: string) => {
-	return resolve(__dirname, '.', dir);
+    return resolve(__dirname, '.', dir);
 };
 
 const alias: Record<string, string> = {
-	'/@': pathResolve('./src/'),
-	'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js',
+    '/@': pathResolve('./src/'),
+    'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js',
 };
 
 const viteConfig = defineConfig((mode: ConfigEnv) => {
-	const env = loadEnv(mode.mode, process.cwd());
-	return {
-		// plugins: [vue(), vueSetupExtend()],
-		plugins: [vue()],
-		root: process.cwd(),
-		resolve: { alias },
-		base: mode.command === 'serve' ? './' : env.VITE_PUBLIC_PATH,
-		optimizeDeps: {
-			include: ['element-plus/es/locale/lang/zh-cn', 'element-plus/es/locale/lang/en', 'element-plus/es/locale/lang/zh-tw'],
-		},
-		server: {
-			host: '0.0.0.0',
-			port: env.VITE_PORT as unknown as number,
-			open: JSON.parse(env.VITE_OPEN),
-			hmr: true,
-		},
-		build: {
-			outDir: '../admin/src/main/resources/static',
-			chunkSizeWarningLimit: 1500,
-			rollupOptions: {
-				output: {
-					entryFileNames: `assets/[name].[hash].js`,
-					chunkFileNames: `assets/[name].[hash].js`,
-					assetFileNames: `assets/[name].[hash].[ext]`,
-					compact: true,
-					manualChunks: {
-						vue: ['vue', 'vue-router', 'pinia'],
-						echarts: ['echarts'],
-					},
-				},
-			},
-		},
-		css: { preprocessorOptions: { css: { charset: false } } },
-		define: {
-			__VUE_I18N_LEGACY_API__: JSON.stringify(false),
-			__VUE_I18N_FULL_INSTALL__: JSON.stringify(false),
-			__INTLIFY_PROD_DEVTOOLS__: JSON.stringify(false),
-			__NEXT_VERSION__: JSON.stringify(process.env.npm_package_version),
-			__NEXT_NAME__: JSON.stringify(process.env.npm_package_name),
-		},
-	};
+    const env = loadEnv(mode.mode, process.cwd());
+    return {
+        // plugins: [vue(), vueSetupExtend()],
+        plugins: [vue()],
+        root: process.cwd(),
+        resolve: {alias},
+        base: mode.command === 'serve' ? './' : env.VITE_PUBLIC_PATH,
+        optimizeDeps: {
+            include: ['element-plus/es/locale/lang/zh-cn', 'element-plus/es/locale/lang/en', 'element-plus/es/locale/lang/zh-tw'],
+        },
+        server: {
+            host: '0.0.0.0',
+            port: env.VITE_PORT as unknown as number,
+            open: JSON.parse(env.VITE_OPEN),
+            hmr: true,
+     /*       proxy: {
+                '/admin': {
+                    target: 'https://www.kuaiyuman.cn/admin/',
+                    changeOrigin: true,
+                    // rewrite: (path) => path.replace(/^\/admin/, '')
+                }
+            }*/
+        },
+        build: {
+            outDir: '../admin/src/main/resources/static',
+            chunkSizeWarningLimit: 1500,
+            rollupOptions: {
+                output: {
+                    entryFileNames: `assets/[name].[hash].js`,
+                    chunkFileNames: `assets/[name].[hash].js`,
+                    assetFileNames: `assets/[name].[hash].[ext]`,
+                    compact: true,
+                    manualChunks: {
+                        vue: ['vue', 'vue-router', 'pinia'],
+                        echarts: ['echarts'],
+                    },
+                },
+            },
+        },
+        css: {preprocessorOptions: {css: {charset: false}}},
+        define: {
+            __VUE_I18N_LEGACY_API__: JSON.stringify(false),
+            __VUE_I18N_FULL_INSTALL__: JSON.stringify(false),
+            __INTLIFY_PROD_DEVTOOLS__: JSON.stringify(false),
+            __NEXT_VERSION__: JSON.stringify(process.env.npm_package_version),
+            __NEXT_NAME__: JSON.stringify(process.env.npm_package_name),
+        },
+    };
 });
 
 export default viteConfig;

+ 2 - 0
service/src/main/java/com/kym/service/miniapp/impl/ChargeOrderServiceImpl.java

@@ -231,6 +231,8 @@ public class ChargeOrderServiceImpl extends MPJBaseServiceImpl<ChargeOrderMapper
     @Override
     @DS("db-admin")
     public Map<String, ?> stationStatDetail(StatQueryParam params) {
+
+        CommUtil.asserts(CommUtil.isNotEmptyAndNull(params.getStationIds()),"站点不能为空");
         if (params.getType().equals(StatQueryParam.TYPE_DAY)) {
             return stationStatDayService.lambdaQuery()
                     .in(StationStatDay::getStationId, params.getStationIds())