index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. <script setup lang="ts">
  2. import { ref, onMounted, reactive } from "vue";
  3. import * as echarts from "echarts";
  4. import { getDeviceOverview, getDeviceList, getDeviceTrend } from "@/api/statistics";
  5. defineOptions({
  6. name: "DeviceStatistics"
  7. });
  8. const loading = ref(false);
  9. const dateRange = ref<[Date, Date]>([
  10. new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
  11. new Date()
  12. ]);
  13. const queryParams = reactive({
  14. page: 1,
  15. pageSize: 10,
  16. keyword: "",
  17. status: null as number | null,
  18. sortBy: "salesAmount",
  19. sortOrder: "desc"
  20. });
  21. const overviewData = ref({
  22. totalDevices: 0,
  23. onlineDevices: 0,
  24. offlineDevices: 0,
  25. onlineRate: "0%",
  26. totalSales: 0,
  27. totalOrders: 0
  28. });
  29. const tableData = ref([]);
  30. const total = ref(0);
  31. const statusChartRef = ref<HTMLElement | null>(null);
  32. const salesChartRef = ref<HTMLElement | null>(null);
  33. let statusChart: echarts.ECharts | null = null;
  34. let salesChart: echarts.ECharts | null = null;
  35. async function fetchOverview() {
  36. try {
  37. const params = {
  38. startDate: formatDate(dateRange.value[0]),
  39. endDate: formatDate(dateRange.value[1])
  40. };
  41. const { data } = await getDeviceOverview(params);
  42. overviewData.value = data;
  43. initStatusChart();
  44. } catch (error) {
  45. console.error("获取设备概览失败:", error);
  46. }
  47. }
  48. async function fetchDeviceList() {
  49. loading.value = true;
  50. try {
  51. const params = {
  52. ...queryParams,
  53. startDate: formatDate(dateRange.value[0]),
  54. endDate: formatDate(dateRange.value[1])
  55. };
  56. const { data } = await getDeviceList(params);
  57. tableData.value = data.list || [];
  58. total.value = data.total || 0;
  59. } catch (error) {
  60. console.error("获取设备统计数据失败:", error);
  61. } finally {
  62. loading.value = false;
  63. }
  64. }
  65. function formatDate(date: Date): string {
  66. return date.toISOString().split("T")[0];
  67. }
  68. function initStatusChart() {
  69. if (!statusChartRef.value) return;
  70. if (statusChart) {
  71. statusChart.dispose();
  72. }
  73. statusChart = echarts.init(statusChartRef.value);
  74. const option: echarts.EChartsOption = {
  75. title: {
  76. text: "设备状态分布",
  77. left: "center",
  78. textStyle: { fontSize: 14, fontWeight: "normal" }
  79. },
  80. tooltip: {
  81. trigger: "item",
  82. formatter: "{a} <br/>{b}: {c} ({d}%)"
  83. },
  84. legend: {
  85. bottom: "5%",
  86. left: "center"
  87. },
  88. series: [
  89. {
  90. name: "设备状态",
  91. type: "pie",
  92. radius: ["40%", "70%"],
  93. avoidLabelOverlap: false,
  94. itemStyle: {
  95. borderRadius: 10,
  96. borderColor: "#fff",
  97. borderWidth: 2
  98. },
  99. label: {
  100. show: false,
  101. position: "center"
  102. },
  103. emphasis: {
  104. label: {
  105. show: true,
  106. fontSize: 20,
  107. fontWeight: "bold"
  108. }
  109. },
  110. labelLine: {
  111. show: false
  112. },
  113. data: [
  114. { value: overviewData.value.onlineDevices, name: "在线", itemStyle: { color: "#67c23a" } },
  115. { value: overviewData.value.offlineDevices, name: "离线", itemStyle: { color: "#909399" } }
  116. ]
  117. }
  118. ]
  119. };
  120. statusChart.setOption(option);
  121. }
  122. async function showDeviceTrend(deviceId: string) {
  123. try {
  124. const params = {
  125. startDate: formatDate(dateRange.value[0]),
  126. endDate: formatDate(dateRange.value[1])
  127. };
  128. const { data } = await getDeviceTrend(deviceId, params);
  129. initSalesChart(data);
  130. } catch (error) {
  131. console.error("获取设备趋势失败:", error);
  132. }
  133. }
  134. function initSalesChart(data: any) {
  135. if (!salesChartRef.value) return;
  136. if (salesChart) {
  137. salesChart.dispose();
  138. }
  139. salesChart = echarts.init(salesChartRef.value);
  140. const option: echarts.EChartsOption = {
  141. title: {
  142. text: "设备销售趋势",
  143. left: "center",
  144. textStyle: { fontSize: 14, fontWeight: "normal" }
  145. },
  146. tooltip: {
  147. trigger: "axis",
  148. backgroundColor: "rgba(255, 255, 255, 0.9)",
  149. borderColor: "#e5e7eb",
  150. borderWidth: 1
  151. },
  152. grid: {
  153. left: "3%",
  154. right: "4%",
  155. bottom: "3%",
  156. containLabel: true
  157. },
  158. xAxis: {
  159. type: "category",
  160. data: data.dates || [],
  161. axisLabel: {
  162. rotate: 45
  163. }
  164. },
  165. yAxis: {
  166. type: "value",
  167. name: "销售额(元)"
  168. },
  169. series: [
  170. {
  171. name: "销售额",
  172. type: "line",
  173. smooth: true,
  174. data: data.series?.[0]?.data || [],
  175. itemStyle: { color: "#409eff" },
  176. areaStyle: {
  177. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  178. { offset: 0, color: "rgba(64, 158, 255, 0.3)" },
  179. { offset: 1, color: "rgba(64, 158, 255, 0.05)" }
  180. ])
  181. }
  182. }
  183. ]
  184. };
  185. salesChart.setOption(option);
  186. }
  187. function handleSearch() {
  188. queryParams.page = 1;
  189. fetchDeviceList();
  190. }
  191. function handleReset() {
  192. queryParams.keyword = "";
  193. queryParams.status = null;
  194. queryParams.page = 1;
  195. fetchDeviceList();
  196. }
  197. function handleSortChange({ prop, order }: any) {
  198. queryParams.sortBy = prop || "salesAmount";
  199. queryParams.sortOrder = order === "ascending" ? "asc" : "desc";
  200. fetchDeviceList();
  201. }
  202. function handlePageChange(page: number) {
  203. queryParams.page = page;
  204. fetchDeviceList();
  205. }
  206. function handleSizeChange(size: number) {
  207. queryParams.pageSize = size;
  208. queryParams.page = 1;
  209. fetchDeviceList();
  210. }
  211. function handleDateChange() {
  212. fetchOverview();
  213. fetchDeviceList();
  214. }
  215. function handleViewTrend(row: any) {
  216. showDeviceTrend(row.deviceId);
  217. }
  218. function resizeCharts() {
  219. statusChart?.resize();
  220. salesChart?.resize();
  221. }
  222. onMounted(() => {
  223. fetchOverview();
  224. fetchDeviceList();
  225. window.addEventListener("resize", resizeCharts);
  226. });
  227. </script>
  228. <template>
  229. <div class="device-statistics">
  230. <el-card shadow="never" class="mb-4">
  231. <div class="flex items-center justify-between">
  232. <span class="text-lg font-medium">设备销售统计</span>
  233. <el-date-picker
  234. v-model="dateRange"
  235. type="daterange"
  236. range-separator="至"
  237. start-placeholder="开始日期"
  238. end-placeholder="结束日期"
  239. format="YYYY-MM-DD"
  240. value-format="YYYY-MM-DD"
  241. @change="handleDateChange"
  242. />
  243. </div>
  244. </el-card>
  245. <el-row :gutter="20" class="mb-6">
  246. <el-col :span="6">
  247. <el-card shadow="hover">
  248. <div class="stat-card">
  249. <div class="stat-icon bg-blue">
  250. <i class="ri:device-line"></i>
  251. </div>
  252. <div class="stat-info">
  253. <div class="stat-value">{{ overviewData.totalDevices }}</div>
  254. <div class="stat-label">设备总数</div>
  255. </div>
  256. </div>
  257. </el-card>
  258. </el-col>
  259. <el-col :span="6">
  260. <el-card shadow="hover">
  261. <div class="stat-card">
  262. <div class="stat-icon bg-green">
  263. <i class="ri:wifi-line"></i>
  264. </div>
  265. <div class="stat-info">
  266. <div class="stat-value">{{ overviewData.onlineDevices }}</div>
  267. <div class="stat-label">在线设备 ({{ overviewData.onlineRate }})</div>
  268. </div>
  269. </div>
  270. </el-card>
  271. </el-col>
  272. <el-col :span="6">
  273. <el-card shadow="hover">
  274. <div class="stat-card">
  275. <div class="stat-icon bg-purple">
  276. <i class="ri:money-cny-circle-line"></i>
  277. </div>
  278. <div class="stat-info">
  279. <div class="stat-value">¥{{ overviewData.totalSales?.toLocaleString() }}</div>
  280. <div class="stat-label">总销售额</div>
  281. </div>
  282. </div>
  283. </el-card>
  284. </el-col>
  285. <el-col :span="6">
  286. <el-card shadow="hover">
  287. <div class="stat-card">
  288. <div class="stat-icon bg-orange">
  289. <i class="ri:shopping-cart-line"></i>
  290. </div>
  291. <div class="stat-info">
  292. <div class="stat-value">{{ overviewData.totalOrders }}</div>
  293. <div class="stat-label">总订单数</div>
  294. </div>
  295. </div>
  296. </el-card>
  297. </el-col>
  298. </el-row>
  299. <el-row :gutter="20" class="mb-6">
  300. <el-col :span="8">
  301. <el-card shadow="hover" class="chart-card">
  302. <div ref="statusChartRef" class="chart-container"></div>
  303. </el-card>
  304. </el-col>
  305. <el-col :span="16">
  306. <el-card shadow="hover" class="chart-card">
  307. <div ref="salesChartRef" class="chart-container"></div>
  308. </el-card>
  309. </el-col>
  310. </el-row>
  311. <el-card shadow="never">
  312. <template #header>
  313. <div class="flex items-center justify-between">
  314. <span class="font-medium">设备销售明细</span>
  315. <div class="flex gap-2">
  316. <el-input
  317. v-model="queryParams.keyword"
  318. placeholder="设备ID/名称"
  319. clearable
  320. style="width: 200px"
  321. @keyup.enter="handleSearch"
  322. />
  323. <el-select v-model="queryParams.status" placeholder="设备状态" clearable style="width: 120px">
  324. <el-option label="在线" :value="1" />
  325. <el-option label="离线" :value="0" />
  326. </el-select>
  327. <el-button type="primary" @click="handleSearch">搜索</el-button>
  328. <el-button @click="handleReset">重置</el-button>
  329. </div>
  330. </div>
  331. </template>
  332. <el-table
  333. :data="tableData"
  334. stripe
  335. v-loading="loading"
  336. @sort-change="handleSortChange"
  337. >
  338. <el-table-column prop="deviceId" label="设备ID" width="150" />
  339. <el-table-column prop="deviceName" label="设备名称" min-width="120" show-overflow-tooltip />
  340. <el-table-column prop="shopName" label="所属门店" min-width="120" show-overflow-tooltip />
  341. <el-table-column prop="status" label="状态" width="80">
  342. <template #default="{ row }">
  343. <el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
  344. {{ row.statusLabel }}
  345. </el-tag>
  346. </template>
  347. </el-table-column>
  348. <el-table-column prop="orderCount" label="订单数" width="80" align="right" sortable="custom" />
  349. <el-table-column prop="userCount" label="购买用户数" width="100" align="right" />
  350. <el-table-column prop="salesAmount" label="销售额(元)" width="120" align="right" sortable="custom">
  351. <template #default="{ row }">
  352. ¥{{ row.salesAmount?.toLocaleString() }}
  353. </template>
  354. </el-table-column>
  355. <el-table-column prop="avgOrderAmount" label="平均客单价" width="100" align="right">
  356. <template #default="{ row }">
  357. ¥{{ row.avgOrderAmount }}
  358. </template>
  359. </el-table-column>
  360. <el-table-column prop="dailySalesAmount" label="日均销售" width="100" align="right">
  361. <template #default="{ row }">
  362. ¥{{ row.dailySalesAmount }}
  363. </template>
  364. </el-table-column>
  365. <el-table-column label="操作" width="100" fixed="right">
  366. <template #default="{ row }">
  367. <el-button type="primary" link size="small" @click="handleViewTrend(row)">
  368. 趋势
  369. </el-button>
  370. </template>
  371. </el-table-column>
  372. </el-table>
  373. <div class="flex justify-end mt-4">
  374. <el-pagination
  375. v-model:current-page="queryParams.page"
  376. v-model:page-size="queryParams.pageSize"
  377. :page-sizes="[10, 20, 50, 100]"
  378. :total="total"
  379. layout="total, sizes, prev, pager, next, jumper"
  380. @size-change="handleSizeChange"
  381. @current-change="handlePageChange"
  382. />
  383. </div>
  384. </el-card>
  385. </div>
  386. </template>
  387. <style lang="scss" scoped>
  388. .device-statistics {
  389. padding: 20px;
  390. background-color: var(--el-bg-color-page);
  391. min-height: calc(100vh - 120px);
  392. }
  393. .stat-card {
  394. display: flex;
  395. align-items: center;
  396. .stat-icon {
  397. width: 50px;
  398. height: 50px;
  399. border-radius: 12px;
  400. display: flex;
  401. align-items: center;
  402. justify-content: center;
  403. margin-right: 16px;
  404. i {
  405. font-size: 24px;
  406. color: white;
  407. }
  408. &.bg-blue { background: linear-gradient(135deg, #409eff, #337ecc); }
  409. &.bg-green { background: linear-gradient(135deg, #67c23a, #529b2e); }
  410. &.bg-purple { background: linear-gradient(135deg, #722ed1, #531dab); }
  411. &.bg-orange { background: linear-gradient(135deg, #fa8c16, #d46b08); }
  412. }
  413. .stat-info {
  414. flex: 1;
  415. .stat-value {
  416. font-size: 24px;
  417. font-weight: 600;
  418. color: var(--el-text-color-primary);
  419. margin-bottom: 4px;
  420. }
  421. .stat-label {
  422. font-size: 14px;
  423. color: var(--el-text-color-secondary);
  424. }
  425. }
  426. }
  427. .chart-card {
  428. height: 300px;
  429. :deep(.el-card__body) {
  430. padding: 20px;
  431. height: 100%;
  432. }
  433. .chart-container {
  434. width: 100%;
  435. height: 100%;
  436. min-height: 250px;
  437. }
  438. }
  439. </style>