Jelajahi Sumber

统计:增加商品统计

owen 1 tahun lalu
induk
melakukan
ca35e1a4af

+ 52 - 0
src/api/mall/statistics/product.ts

@@ -0,0 +1,52 @@
+import request from '@/config/axios'
+import { DataComparisonRespVO } from '@/api/mall/statistics/common'
+
+export interface ProductStatisticsVO {
+  id: number
+  day: string
+  spuId: number
+  spuName: string
+  spuPicUrl: string
+  browseCount: number
+  browseUserCount: number
+  favoriteCount: number
+  cartCount: number
+  orderCount: number
+  orderPayCount: number
+  orderPayPrice: number
+  afterSaleCount: number
+  afterSaleRefundPrice: number
+  browseConvertPercent: number
+}
+
+// 商品统计 API
+export const ProductStatisticsApi = {
+  // 获得商品统计分析
+  getProductStatisticsAnalyse: (params: any) => {
+    return request.get<DataComparisonRespVO<ProductStatisticsVO>>({
+      url: '/statistics/product/analyse',
+      params
+    })
+  },
+  // 获得商品状况明细
+  getProductStatisticsList: (params: any) => {
+    return request.get<ProductStatisticsVO[]>({
+      url: '/statistics/product/list',
+      params
+    })
+  },
+  // 导出获得商品状况明细 Excel
+  exportProductStatisticsExcel: (params: any) => {
+    return request.download({
+      url: '/statistics/product/export-excel',
+      params
+    })
+  },
+  // 获得商品排行榜分页
+  getProductStatisticsRankPage: async (params: any) => {
+    return await request.get({
+      url: `/statistics/product/rank-page`,
+      params
+    })
+  }
+}

+ 2 - 2
src/api/mall/statistics/trade.ts

@@ -66,9 +66,9 @@ export const getTradeStatisticsSummary = () => {
 }
 
 // 获得交易状况统计
-export const getTradeTrendSummary = (params: TradeTrendReqVO) => {
+export const getTradeStatisticsAnalyse = (params: TradeTrendReqVO) => {
   return request.get<DataComparisonRespVO<TradeTrendSummaryRespVO>>({
-    url: '/statistics/trade/trend/summary',
+    url: '/statistics/trade/analyse',
     params: formatDateParam(params)
   })
 }

+ 4 - 20
src/config/axios/service.ts

@@ -70,27 +70,11 @@ service.interceptors.request.use(
     }
     // get参数编码
     if (config.method?.toUpperCase() === 'GET' && params) {
-      let url = config.url + '?'
-      for (const propName of Object.keys(params)) {
-        const value = params[propName]
-        if (value !== void 0 && value !== null && typeof value !== 'undefined') {
-          if (typeof value === 'object') {
-            for (const val of Object.keys(value)) {
-              const params = propName + '[' + val + ']'
-              const subPart = encodeURIComponent(params) + '='
-              url += subPart + encodeURIComponent(value[val]) + '&'
-            }
-          } else {
-            url += `${propName}=${encodeURIComponent(value)}&`
-          }
-        }
-      }
-      // 给 get 请求加上时间戳参数,避免从缓存中拿数据
-      // const now = new Date().getTime()
-      // params = params.substring(0, url.length - 1) + `?_t=${now}`
-      url = url.slice(0, -1)
       config.params = {}
-      config.url = url
+      const paramsStr = qs.stringify(params, { allowDots: true })
+      if (paramsStr) {
+        config.url = config.url + '?' + paramsStr
+      }
     }
     return config
   },

+ 9 - 0
src/utils/index.ts

@@ -285,3 +285,12 @@ export const getUrlValue = (key: string, urlStr: string = location.href): string
 export const getUrlNumberValue = (key: string, urlStr: string = location.href): number => {
   return toNumber(getUrlValue(key, urlStr))
 }
+
+/**
+ * 构建排序字段
+ * @param prop 字段名称
+ * @param order 顺序
+ */
+export const buildSortingField = ({ prop, order }) => {
+  return { field: prop, order: order === 'ascending' ? 'asc' : 'desc' }
+}

+ 104 - 0
src/views/mall/statistics/product/components/ProductRank.vue

@@ -0,0 +1,104 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <!-- 标题 -->
+      <div class="flex flex-row items-center justify-between">
+        <CardTitle title="商品排行" />
+        <!-- 查询条件 -->
+        <ShortcutDateRangePicker ref="shortcutDateRangePicker" @change="handleDateRangeChange" />
+      </div>
+    </template>
+    <!-- 排行列表 -->
+    <el-table v-loading="loading" :data="list" @sort-change="handleSortChange">
+      <el-table-column label="商品ID" prop="spuId" min-width="70" />
+      <el-table-column label="商品图片" align="center" prop="picUrl" width="80">
+        <template #default="{ row }">
+          <el-image
+            :src="row.picUrl"
+            :preview-src-list="[row.picUrl]"
+            class="h-30px w-30px"
+            preview-teleported
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="商品名称" prop="name" min-width="200" :show-overflow-tooltip="true" />
+      <el-table-column label="浏览量" prop="browseCount" min-width="90" sortable="custom" />
+      <el-table-column label="访客数" prop="browseUserCount" min-width="90" sortable="custom" />
+      <el-table-column label="加购件数" prop="cartCount" min-width="105" sortable="custom" />
+      <el-table-column label="下单件数" prop="orderCount" min-width="105" sortable="custom" />
+      <el-table-column label="支付件数" prop="orderPayCount" min-width="105" sortable="custom" />
+      <el-table-column label="支付金额" prop="orderPayPrice" min-width="105" sortable="custom" />
+      <el-table-column label="收藏数" prop="favoriteCount" min-width="90" sortable="custom" />
+      <el-table-column
+        label="访客-支付转化率(%)"
+        prop="browseConvertPercent"
+        min-width="180"
+        sortable="custom"
+        :formatter="formatConvertRate"
+      />
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getSpuList"
+    />
+  </el-card>
+</template>
+<script lang="ts" setup>
+import { ProductStatisticsApi, ProductStatisticsVO } from '@/api/mall/statistics/product'
+import { CardTitle } from '@/components/Card'
+import { buildSortingField } from '@/utils'
+
+/** 商品排行 */
+defineOptions({ name: 'ProductRank' })
+
+// 格式化:访客-支付转化率
+const formatConvertRate = (row: ProductStatisticsVO) => {
+  return `${row.browseConvertPercent}%`
+}
+
+const handleSortChange = (params: any) => {
+  queryParams.sortingFields = [buildSortingField(params)]
+  getSpuList()
+}
+
+const handleDateRangeChange = (times: any[]) => {
+  queryParams.times = times as []
+  getSpuList()
+}
+
+const shortcutDateRangePicker = ref()
+// 查询参数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  times: [],
+  sortingFields: {}
+})
+// 列表的加载中
+const loading = ref(false)
+// 列表的总页数
+const total = ref(0)
+// 列表的数据
+const list = ref<ProductStatisticsVO[]>([])
+
+/** 查询商品列表 */
+const getSpuList = async () => {
+  loading.value = true
+  try {
+    const data = await ProductStatisticsApi.getProductStatisticsRankPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getSpuList()
+})
+</script>
+<style lang="scss" scoped></style>

+ 304 - 0
src/views/mall/statistics/product/components/ProductSummary.vue

@@ -0,0 +1,304 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <!-- 标题 -->
+      <div class="flex flex-row items-center justify-between">
+        <CardTitle title="商品概况" />
+        <!-- 查询条件 -->
+        <ShortcutDateRangePicker ref="shortcutDateRangePicker" @change="getProductTrendData">
+          <el-button
+            class="ml-4"
+            @click="handleExport"
+            :loading="exportLoading"
+            v-hasPermi="['statistics:product:export']"
+          >
+            <Icon icon="ep:download" class="mr-1" />导出
+          </el-button>
+        </ShortcutDateRangePicker>
+      </div>
+    </template>
+    <!-- 统计值 -->
+    <el-row :gutter="16">
+      <el-col :xl="4" :md="8" :sm="24">
+        <SummaryCard
+          title="商品浏览量"
+          tooltip="在选定条件下,所有商品详情页被访问的次数,一个人在统计时间内访问多次记为多次"
+          icon="ep:view"
+          icon-color="bg-blue-100"
+          icon-bg-color="text-blue-500"
+          prefix="¥"
+          :decimals="2"
+          :value="fenToYuan(trendSummary?.value?.browseCount || 0)"
+          :percent="
+            calculateRelativeRate(
+              trendSummary?.value?.browseCount,
+              trendSummary?.reference?.browseCount
+            )
+          "
+        />
+      </el-col>
+      <el-col :xl="4" :md="8" :sm="24">
+        <SummaryCard
+          title="商品访客数"
+          tooltip="在选定条件下,访问任何商品详情页的人数,一个人在统计时间范围内访问多次只记为一个"
+          icon="ep:user-filled"
+          icon-color="bg-purple-100"
+          icon-bg-color="text-purple-500"
+          prefix="¥"
+          :decimals="2"
+          :value="fenToYuan(trendSummary?.value?.browseUserCount || 0)"
+          :percent="
+            calculateRelativeRate(
+              trendSummary?.value?.browseUserCount,
+              trendSummary?.reference?.browseUserCount
+            )
+          "
+        />
+      </el-col>
+      <el-col :xl="4" :md="8" :sm="24">
+        <SummaryCard
+          title="支付件数"
+          tooltip="在选定条件下,成功付款订单的商品件数之和"
+          icon="fa-solid:money-check-alt"
+          icon-color="bg-yellow-100"
+          icon-bg-color="text-yellow-500"
+          prefix="¥"
+          :decimals="2"
+          :value="fenToYuan(trendSummary?.value?.orderPayCount || 0)"
+          :percent="
+            calculateRelativeRate(
+              trendSummary?.value?.orderPayCount,
+              trendSummary?.reference?.orderPayCount
+            )
+          "
+        />
+      </el-col>
+      <el-col :xl="4" :md="8" :sm="24">
+        <SummaryCard
+          title="支付金额"
+          tooltip="在选定条件下,成功付款订单的商品金额之和"
+          icon="ep:warning-filled"
+          icon-color="bg-green-100"
+          icon-bg-color="text-green-500"
+          prefix="¥"
+          :decimals="2"
+          :value="fenToYuan(trendSummary?.value?.orderPayPrice || 0)"
+          :percent="
+            calculateRelativeRate(
+              trendSummary?.value?.orderPayPrice,
+              trendSummary?.reference?.orderPayPrice
+            )
+          "
+        />
+      </el-col>
+      <el-col :xl="4" :md="8" :sm="24">
+        <SummaryCard
+          title="退款件数"
+          tooltip="在选定条件下,成功退款的商品件数之和"
+          icon="fa-solid:wallet"
+          icon-color="bg-cyan-100"
+          icon-bg-color="text-cyan-500"
+          prefix="¥"
+          :decimals="2"
+          :value="fenToYuan(trendSummary?.value?.afterSaleCount || 0)"
+          :percent="
+            calculateRelativeRate(
+              trendSummary?.value?.afterSaleCount,
+              trendSummary?.reference?.afterSaleCount
+            )
+          "
+        />
+      </el-col>
+      <el-col :xl="4" :md="8" :sm="24">
+        <SummaryCard
+          title="退款金额"
+          tooltip="在选定条件下,成功退款的商品金额之和"
+          icon="fa-solid:award"
+          icon-color="bg-yellow-100"
+          icon-bg-color="text-yellow-500"
+          prefix="¥"
+          :decimals="2"
+          :value="fenToYuan(trendSummary?.value?.afterSaleRefundPrice || 0)"
+          :percent="
+            calculateRelativeRate(
+              trendSummary?.value?.afterSaleRefundPrice,
+              trendSummary?.reference?.afterSaleRefundPrice
+            )
+          "
+        />
+      </el-col>
+    </el-row>
+    <!-- 折线图 -->
+    <el-skeleton :loading="trendLoading" animated>
+      <Echart :height="500" :options="lineChartOptions" />
+    </el-skeleton>
+  </el-card>
+</template>
+<script lang="ts" setup>
+import { ProductStatisticsApi, ProductStatisticsVO } from '@/api/mall/statistics/product'
+import SummaryCard from '@/components/SummaryCard/index.vue'
+import { EChartsOption } from 'echarts'
+import { DataComparisonRespVO } from '@/api/mall/statistics/common'
+import { calculateRelativeRate, fenToYuan } from '@/utils'
+import download from '@/utils/download'
+import { CardTitle } from '@/components/Card'
+import * as DateUtil from '@/utils/formatTime'
+import dayjs from 'dayjs'
+
+/** 商品概况 */
+defineOptions({ name: 'ProductSummary' })
+
+const message = useMessage() // 消息弹窗
+
+const trendLoading = ref(true) // 商品状态加载中
+const exportLoading = ref(false) // 导出的加载中
+const trendSummary = ref<DataComparisonRespVO<ProductStatisticsVO>>() // 商品状况统计数据
+const shortcutDateRangePicker = ref()
+
+/** 折线图配置 */
+const lineChartOptions = reactive<EChartsOption>({
+  dataset: {
+    dimensions: ['time', 'browseCount', 'browseUserCount', 'orderPayPrice', 'afterSaleRefundPrice'],
+    source: []
+  },
+  grid: {
+    left: 20,
+    right: 20,
+    bottom: 20,
+    top: 80,
+    containLabel: true
+  },
+  legend: {
+    top: 50
+  },
+  series: [
+    { name: '商品浏览量', type: 'line', smooth: true, itemStyle: { color: '#B37FEB' } },
+    { name: '商品访客数', type: 'line', smooth: true, itemStyle: { color: '#FFAB2B' } },
+    { name: '支付金额', type: 'bar', smooth: true, yAxisIndex: 1, itemStyle: { color: '#1890FF' } },
+    { name: '退款金额', type: 'bar', smooth: true, yAxisIndex: 1, itemStyle: { color: '#00C050' } }
+  ],
+  toolbox: {
+    feature: {
+      // 数据区域缩放
+      dataZoom: {
+        yAxisIndex: false // Y轴不缩放
+      },
+      brush: {
+        type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
+      },
+      saveAsImage: { show: true, name: '商品状况' } // 保存为图片
+    }
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'cross'
+    },
+    padding: [5, 10]
+  },
+  xAxis: {
+    type: 'category',
+    boundaryGap: true,
+    axisTick: {
+      show: false
+    }
+  },
+  yAxis: [
+    {
+      type: 'value',
+      name: '金额',
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        textStyle: {
+          color: '#7F8B9C'
+        }
+      },
+      splitLine: {
+        show: true,
+        lineStyle: {
+          color: '#F5F7F9'
+        }
+      }
+    },
+    {
+      type: 'value',
+      name: '数量',
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        textStyle: {
+          color: '#7F8B9C'
+        }
+      },
+      splitLine: {
+        show: true,
+        lineStyle: {
+          color: '#F5F7F9'
+        }
+      }
+    }
+  ]
+}) as EChartsOption
+
+/** 处理商品状况查询 */
+const getProductTrendData = async () => {
+  trendLoading.value = true
+  // 1. 处理时间: 开始与截止在同一天的, 折线图出不来, 需要延长一天
+  const times = shortcutDateRangePicker.value.times
+  if (DateUtil.isSameDay(times[0], times[1])) {
+    // 前天
+    times[0] = DateUtil.formatDate(dayjs(times[0]).subtract(1, 'd'))
+  }
+  // 查询数据
+  await Promise.all([getProductTrendSummary(), getProductStatisticsList()])
+  trendLoading.value = false
+}
+
+/** 查询商品状况数据统计 */
+const getProductTrendSummary = async () => {
+  const times = shortcutDateRangePicker.value.times
+  trendSummary.value = await ProductStatisticsApi.getProductStatisticsAnalyse({ times })
+}
+
+/** 查询商品状况数据列表 */
+const getProductStatisticsList = async () => {
+  // 查询数据
+  const times = shortcutDateRangePicker.value.times
+  const list: ProductStatisticsVO[] = await ProductStatisticsApi.getProductStatisticsList({ times })
+  // 处理数据
+  for (let item of list) {
+    item.orderPayPrice = fenToYuan(item.orderPayPrice)
+    item.afterSaleRefundPrice = fenToYuan(item.afterSaleRefundPrice)
+  }
+  // 更新 Echarts 数据
+  if (lineChartOptions.dataset && lineChartOptions.dataset['source']) {
+    lineChartOptions.dataset['source'] = list
+  }
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const times = shortcutDateRangePicker.value.times
+    const data = await ProductStatisticsApi.exportProductStatisticsExcel({ times })
+    download.excel(data, '商品状况.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+</script>
+<style lang="scss" scoped></style>

+ 14 - 0
src/views/mall/statistics/product/index.vue

@@ -0,0 +1,14 @@
+<template>
+  <!-- 商品概览 -->
+  <ProductSummary />
+  <!-- 商品排行 -->
+  <ProductRank class="mt-16px" />
+</template>
+<script lang="ts" setup>
+import ProductSummary from './components/ProductSummary.vue'
+import ProductRank from './components/ProductRank.vue'
+
+/** 商品统计 */
+defineOptions({ name: 'ProductStatistics' })
+</script>
+<style lang="scss" scoped></style>

+ 3 - 3
src/views/mall/statistics/trade/index.vue

@@ -298,7 +298,7 @@ const getTradeTrendData = async () => {
     times[0] = DateUtil.formatDate(dayjs(times[0]).subtract(1, 'd'))
   }
   // 查询数据
-  await Promise.all([getTradeTrendSummary(), getTradeStatisticsList()])
+  await Promise.all([getTradeStatisticsAnalyse(), getTradeStatisticsList()])
   trendLoading.value = false
 }
 
@@ -308,9 +308,9 @@ const getTradeStatisticsSummary = async () => {
 }
 
 /** 查询交易状况数据统计 */
-const getTradeTrendSummary = async () => {
+const getTradeStatisticsAnalyse = async () => {
   const times = shortcutDateRangePicker.value.times
-  trendSummary.value = await TradeStatisticsApi.getTradeTrendSummary({ times })
+  trendSummary.value = await TradeStatisticsApi.getTradeStatisticsAnalyse({ times })
 }
 
 /** 查询交易状况数据列表 */