云
4 天以前 2f9c84cf0e0c03a8e3bf07d3924ccdf791070b77
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
 
import { Page } from '@vben/common-ui';
 
import { Card, Col, DatePicker, Input, Row, Table } from 'ant-design-vue';
import dayjs from 'dayjs';
 
import { getRangePickerDefaultProps } from '#/utils';
import { getTraceLocationDistribution, getTraceLocationVisits } from '#/api/mes/trace-consumer-scan';
import type { MesTraceConsumerScanApi } from '#/api/mes/trace-consumer-scan';
 
import DistributionChart from './modules/distribution-chart.vue';
import SummaryCards from './modules/summary-cards.vue';
import TrendChart from './modules/trend-chart.vue';
 
defineOptions({ name: 'MesConsumerScanStatistics' });
 
/** 统计时间范围,默认近 30 天(valueFormat 已转为 yyyy-MM-dd HH:mm:ss) */
const dateRange = ref<[string, string] | undefined>([
  dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'),
  dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss'),
]);
 
const startTime = computed(() => dateRange.value?.[0]);
const endTime = computed(() => dateRange.value?.[1]);
 
const traceCode = ref('');
const pageNo = ref(1);
const locationLoading = ref(false);
const locationRows = ref<MesTraceConsumerScanApi.LocationVisit[]>([]);
const locationDistribution = ref<MesTraceConsumerScanApi.LocationDistribution[]>([]);
const locationTotal = ref(0);
const locationColumns = [
  { title: '访问时间', dataIndex: 'visitTime', key: 'visitTime' },
  { title: '追溯码', dataIndex: 'traceCode', key: 'traceCode' },
  { title: '纬度', dataIndex: 'latitude', key: 'latitude' },
  { title: '经度', dataIndex: 'longitude', key: 'longitude' },
  { title: '精度(米)', dataIndex: 'accuracy', key: 'accuracy' },
  { title: '位置', key: 'map' },
];
 
async function loadLocationVisits() {
  locationLoading.value = true;
  try {
    const page = await getTraceLocationVisits({
      pageNo: pageNo.value,
      pageSize: 20,
      startTime: startTime.value,
      endTime: endTime.value,
      traceCode: traceCode.value.trim() || undefined,
    });
    locationRows.value = page.list;
    locationTotal.value = page.total;
    locationDistribution.value = await getTraceLocationDistribution({
      startTime: startTime.value,
      endTime: endTime.value,
      traceCode: traceCode.value.trim() || undefined,
    });
  } finally {
    locationLoading.value = false;
  }
}
 
watch([startTime, endTime, traceCode], () => {
  pageNo.value = 1;
  loadLocationVisits();
}, { immediate: true });
 
function mapUrl(row: Record<string, any>) {
  const lat = Number(row.latitude);
  const lon = Number(row.longitude);
  return `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}#map=16/${lat}/${lon}`;
}
 
function handleLocationPageChange(pagination: { current?: number }) {
  pageNo.value = pagination.current || 1;
  loadLocationVisits();
}
</script>
 
<template>
  <Page>
    <Card title="消费者扫码统计" class="mb-4">
      <div class="flex flex-wrap items-center gap-2">
        <span class="text-muted-foreground text-sm">统计时间范围</span>
        <DatePicker.RangePicker
          v-model:value="dateRange"
          v-bind="getRangePickerDefaultProps()"
        />
      </div>
    </Card>
 
    <SummaryCards :start-time="startTime" :end-time="endTime" />
 
    <Row :gutter="16">
      <Col :lg="14" :md="24" :sm="24" :xl="14" :xs="24" class="mb-4">
        <TrendChart :start-time="startTime" :end-time="endTime" />
      </Col>
      <Col :lg="10" :md="24" :sm="24" :xl="10" :xs="24" class="mb-4">
        <DistributionChart :start-time="startTime" :end-time="endTime" />
      </Col>
    </Row>
 
    <Card title="定位授权访问" class="mb-4">
      <p class="mb-3 text-sm text-muted-foreground">
        当前筛选条件下共 {{ locationTotal }} 次授权定位访问;下表显示用户授权后上传的位置及定位精度。
      </p>
      <Input v-model:value="traceCode" class="mb-3 max-w-xs" allow-clear placeholder="按追溯码筛选" />
      <div v-if="locationDistribution.length" class="mb-4">
        <div class="mb-2 font-medium">热门位置(坐标保留两位小数,最多 20 处)</div>
        <div class="flex flex-wrap gap-2">
          <span v-for="item in locationDistribution" :key="`${item.latitude},${item.longitude}`" class="rounded bg-green-50 px-3 py-1 text-sm text-green-800">
            {{ item.latitude }}, {{ item.longitude }}:{{ item.count }} 次
          </span>
        </div>
      </div>
      <Table
        :columns="locationColumns"
        :data-source="locationRows"
        :loading="locationLoading"
        :pagination="{ current: pageNo, pageSize: 20, total: locationTotal, showSizeChanger: false }"
        :row-key="(row: MesTraceConsumerScanApi.LocationVisit) => row.id"
        :scroll="{ x: 850 }"
        @change="handleLocationPageChange"
      >
        <template #bodyCell="{ column, record }">
          <a v-if="column.key === 'map'" :href="mapUrl(record)" target="_blank" rel="noopener noreferrer">查看地图</a>
        </template>
      </Table>
    </Card>
  </Page>
</template>