| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488 |
- <script setup lang="ts">
- import { ref, onMounted, reactive } from "vue";
- import * as echarts from "echarts";
- import { getDeviceOverview, getDeviceList, getDeviceTrend } from "@/api/statistics";
- defineOptions({
- name: "DeviceStatistics"
- });
- const loading = ref(false);
- const dateRange = ref<[Date, Date]>([
- new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
- new Date()
- ]);
- const queryParams = reactive({
- page: 1,
- pageSize: 10,
- keyword: "",
- status: null as number | null,
- sortBy: "salesAmount",
- sortOrder: "desc"
- });
- const overviewData = ref({
- totalDevices: 0,
- onlineDevices: 0,
- offlineDevices: 0,
- onlineRate: "0%",
- totalSales: 0,
- totalOrders: 0
- });
- const tableData = ref([]);
- const total = ref(0);
- const statusChartRef = ref<HTMLElement | null>(null);
- const salesChartRef = ref<HTMLElement | null>(null);
- let statusChart: echarts.ECharts | null = null;
- let salesChart: echarts.ECharts | null = null;
- async function fetchOverview() {
- try {
- const params = {
- startDate: formatDate(dateRange.value[0]),
- endDate: formatDate(dateRange.value[1])
- };
-
- const { data } = await getDeviceOverview(params);
- overviewData.value = data;
- initStatusChart();
- } catch (error) {
- console.error("获取设备概览失败:", error);
- }
- }
- async function fetchDeviceList() {
- loading.value = true;
- try {
- const params = {
- ...queryParams,
- startDate: formatDate(dateRange.value[0]),
- endDate: formatDate(dateRange.value[1])
- };
-
- const { data } = await getDeviceList(params);
- tableData.value = data.list || [];
- total.value = data.total || 0;
- } catch (error) {
- console.error("获取设备统计数据失败:", error);
- } finally {
- loading.value = false;
- }
- }
- function formatDate(date: Date): string {
- return date.toISOString().split("T")[0];
- }
- function initStatusChart() {
- if (!statusChartRef.value) return;
-
- if (statusChart) {
- statusChart.dispose();
- }
-
- statusChart = echarts.init(statusChartRef.value);
-
- const option: echarts.EChartsOption = {
- title: {
- text: "设备状态分布",
- left: "center",
- textStyle: { fontSize: 14, fontWeight: "normal" }
- },
- tooltip: {
- trigger: "item",
- formatter: "{a} <br/>{b}: {c} ({d}%)"
- },
- legend: {
- bottom: "5%",
- left: "center"
- },
- series: [
- {
- name: "设备状态",
- type: "pie",
- radius: ["40%", "70%"],
- avoidLabelOverlap: false,
- itemStyle: {
- borderRadius: 10,
- borderColor: "#fff",
- borderWidth: 2
- },
- label: {
- show: false,
- position: "center"
- },
- emphasis: {
- label: {
- show: true,
- fontSize: 20,
- fontWeight: "bold"
- }
- },
- labelLine: {
- show: false
- },
- data: [
- { value: overviewData.value.onlineDevices, name: "在线", itemStyle: { color: "#67c23a" } },
- { value: overviewData.value.offlineDevices, name: "离线", itemStyle: { color: "#909399" } }
- ]
- }
- ]
- };
-
- statusChart.setOption(option);
- }
- async function showDeviceTrend(deviceId: string) {
- try {
- const params = {
- startDate: formatDate(dateRange.value[0]),
- endDate: formatDate(dateRange.value[1])
- };
-
- const { data } = await getDeviceTrend(deviceId, params);
- initSalesChart(data);
- } catch (error) {
- console.error("获取设备趋势失败:", error);
- }
- }
- function initSalesChart(data: any) {
- if (!salesChartRef.value) return;
-
- if (salesChart) {
- salesChart.dispose();
- }
-
- salesChart = echarts.init(salesChartRef.value);
-
- const option: echarts.EChartsOption = {
- title: {
- text: "设备销售趋势",
- left: "center",
- textStyle: { fontSize: 14, fontWeight: "normal" }
- },
- tooltip: {
- trigger: "axis",
- backgroundColor: "rgba(255, 255, 255, 0.9)",
- borderColor: "#e5e7eb",
- borderWidth: 1
- },
- grid: {
- left: "3%",
- right: "4%",
- bottom: "3%",
- containLabel: true
- },
- xAxis: {
- type: "category",
- data: data.dates || [],
- axisLabel: {
- rotate: 45
- }
- },
- yAxis: {
- type: "value",
- name: "销售额(元)"
- },
- series: [
- {
- name: "销售额",
- type: "line",
- smooth: true,
- data: data.series?.[0]?.data || [],
- itemStyle: { color: "#409eff" },
- areaStyle: {
- color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
- { offset: 0, color: "rgba(64, 158, 255, 0.3)" },
- { offset: 1, color: "rgba(64, 158, 255, 0.05)" }
- ])
- }
- }
- ]
- };
-
- salesChart.setOption(option);
- }
- function handleSearch() {
- queryParams.page = 1;
- fetchDeviceList();
- }
- function handleReset() {
- queryParams.keyword = "";
- queryParams.status = null;
- queryParams.page = 1;
- fetchDeviceList();
- }
- function handleSortChange({ prop, order }: any) {
- queryParams.sortBy = prop || "salesAmount";
- queryParams.sortOrder = order === "ascending" ? "asc" : "desc";
- fetchDeviceList();
- }
- function handlePageChange(page: number) {
- queryParams.page = page;
- fetchDeviceList();
- }
- function handleSizeChange(size: number) {
- queryParams.pageSize = size;
- queryParams.page = 1;
- fetchDeviceList();
- }
- function handleDateChange() {
- fetchOverview();
- fetchDeviceList();
- }
- function handleViewTrend(row: any) {
- showDeviceTrend(row.deviceId);
- }
- function resizeCharts() {
- statusChart?.resize();
- salesChart?.resize();
- }
- onMounted(() => {
- fetchOverview();
- fetchDeviceList();
- window.addEventListener("resize", resizeCharts);
- });
- </script>
- <template>
- <div class="device-statistics">
- <el-card shadow="never" class="mb-4">
- <div class="flex items-center justify-between">
- <span class="text-lg font-medium">设备销售统计</span>
- <el-date-picker
- v-model="dateRange"
- type="daterange"
- range-separator="至"
- start-placeholder="开始日期"
- end-placeholder="结束日期"
- format="YYYY-MM-DD"
- value-format="YYYY-MM-DD"
- @change="handleDateChange"
- />
- </div>
- </el-card>
- <el-row :gutter="20" class="mb-6">
- <el-col :span="6">
- <el-card shadow="hover">
- <div class="stat-card">
- <div class="stat-icon bg-blue">
- <i class="ri:device-line"></i>
- </div>
- <div class="stat-info">
- <div class="stat-value">{{ overviewData.totalDevices }}</div>
- <div class="stat-label">设备总数</div>
- </div>
- </div>
- </el-card>
- </el-col>
- <el-col :span="6">
- <el-card shadow="hover">
- <div class="stat-card">
- <div class="stat-icon bg-green">
- <i class="ri:wifi-line"></i>
- </div>
- <div class="stat-info">
- <div class="stat-value">{{ overviewData.onlineDevices }}</div>
- <div class="stat-label">在线设备 ({{ overviewData.onlineRate }})</div>
- </div>
- </div>
- </el-card>
- </el-col>
- <el-col :span="6">
- <el-card shadow="hover">
- <div class="stat-card">
- <div class="stat-icon bg-purple">
- <i class="ri:money-cny-circle-line"></i>
- </div>
- <div class="stat-info">
- <div class="stat-value">¥{{ overviewData.totalSales?.toLocaleString() }}</div>
- <div class="stat-label">总销售额</div>
- </div>
- </div>
- </el-card>
- </el-col>
- <el-col :span="6">
- <el-card shadow="hover">
- <div class="stat-card">
- <div class="stat-icon bg-orange">
- <i class="ri:shopping-cart-line"></i>
- </div>
- <div class="stat-info">
- <div class="stat-value">{{ overviewData.totalOrders }}</div>
- <div class="stat-label">总订单数</div>
- </div>
- </div>
- </el-card>
- </el-col>
- </el-row>
- <el-row :gutter="20" class="mb-6">
- <el-col :span="8">
- <el-card shadow="hover" class="chart-card">
- <div ref="statusChartRef" class="chart-container"></div>
- </el-card>
- </el-col>
- <el-col :span="16">
- <el-card shadow="hover" class="chart-card">
- <div ref="salesChartRef" class="chart-container"></div>
- </el-card>
- </el-col>
- </el-row>
- <el-card shadow="never">
- <template #header>
- <div class="flex items-center justify-between">
- <span class="font-medium">设备销售明细</span>
- <div class="flex gap-2">
- <el-input
- v-model="queryParams.keyword"
- placeholder="设备ID/名称"
- clearable
- style="width: 200px"
- @keyup.enter="handleSearch"
- />
- <el-select v-model="queryParams.status" placeholder="设备状态" clearable style="width: 120px">
- <el-option label="在线" :value="1" />
- <el-option label="离线" :value="0" />
- </el-select>
- <el-button type="primary" @click="handleSearch">搜索</el-button>
- <el-button @click="handleReset">重置</el-button>
- </div>
- </div>
- </template>
-
- <el-table
- :data="tableData"
- stripe
- v-loading="loading"
- @sort-change="handleSortChange"
- >
- <el-table-column prop="deviceId" label="设备ID" width="150" />
- <el-table-column prop="deviceName" label="设备名称" min-width="120" show-overflow-tooltip />
- <el-table-column prop="shopName" label="所属门店" min-width="120" show-overflow-tooltip />
- <el-table-column prop="status" label="状态" width="80">
- <template #default="{ row }">
- <el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
- {{ row.statusLabel }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="orderCount" label="订单数" width="80" align="right" sortable="custom" />
- <el-table-column prop="userCount" label="购买用户数" width="100" align="right" />
- <el-table-column prop="salesAmount" label="销售额(元)" width="120" align="right" sortable="custom">
- <template #default="{ row }">
- ¥{{ row.salesAmount?.toLocaleString() }}
- </template>
- </el-table-column>
- <el-table-column prop="avgOrderAmount" label="平均客单价" width="100" align="right">
- <template #default="{ row }">
- ¥{{ row.avgOrderAmount }}
- </template>
- </el-table-column>
- <el-table-column prop="dailySalesAmount" label="日均销售" width="100" align="right">
- <template #default="{ row }">
- ¥{{ row.dailySalesAmount }}
- </template>
- </el-table-column>
- <el-table-column label="操作" width="100" fixed="right">
- <template #default="{ row }">
- <el-button type="primary" link size="small" @click="handleViewTrend(row)">
- 趋势
- </el-button>
- </template>
- </el-table-column>
- </el-table>
-
- <div class="flex justify-end mt-4">
- <el-pagination
- v-model:current-page="queryParams.page"
- v-model:page-size="queryParams.pageSize"
- :page-sizes="[10, 20, 50, 100]"
- :total="total"
- layout="total, sizes, prev, pager, next, jumper"
- @size-change="handleSizeChange"
- @current-change="handlePageChange"
- />
- </div>
- </el-card>
- </div>
- </template>
- <style lang="scss" scoped>
- .device-statistics {
- padding: 20px;
- background-color: var(--el-bg-color-page);
- min-height: calc(100vh - 120px);
- }
- .stat-card {
- display: flex;
- align-items: center;
-
- .stat-icon {
- width: 50px;
- height: 50px;
- border-radius: 12px;
- display: flex;
- align-items: center;
- justify-content: center;
- margin-right: 16px;
-
- i {
- font-size: 24px;
- color: white;
- }
-
- &.bg-blue { background: linear-gradient(135deg, #409eff, #337ecc); }
- &.bg-green { background: linear-gradient(135deg, #67c23a, #529b2e); }
- &.bg-purple { background: linear-gradient(135deg, #722ed1, #531dab); }
- &.bg-orange { background: linear-gradient(135deg, #fa8c16, #d46b08); }
- }
-
- .stat-info {
- flex: 1;
-
- .stat-value {
- font-size: 24px;
- font-weight: 600;
- color: var(--el-text-color-primary);
- margin-bottom: 4px;
- }
-
- .stat-label {
- font-size: 14px;
- color: var(--el-text-color-secondary);
- }
- }
- }
- .chart-card {
- height: 300px;
-
- :deep(.el-card__body) {
- padding: 20px;
- height: 100%;
- }
-
- .chart-container {
- width: 100%;
- height: 100%;
- min-height: 250px;
- }
- }
- </style>
|