index.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772
  1. <script setup lang="ts">
  2. import { ref, onMounted, computed, watch, nextTick } from "vue";
  3. import * as echarts from "echarts/core";
  4. import { BarChart, LineChart, PieChart } from "echarts/charts";
  5. import {
  6. GridComponent,
  7. TooltipComponent,
  8. LegendComponent,
  9. TitleComponent
  10. } from "echarts/components";
  11. import { CanvasRenderer } from "echarts/renderers";
  12. import {
  13. getOverview,
  14. getStatistics,
  15. getQuickLinks,
  16. getDeviceStatus
  17. } from "@/api/dashboard";
  18. import RefreshIcon from "~icons/ep/refresh";
  19. import DeviceIcon from "~icons/ri/device-line";
  20. import OrderIcon from "~icons/ri/shopping-cart-line";
  21. import UserIcon from "~icons/ri/user-3-line";
  22. import StackIcon from "~icons/ri/stack-line";
  23. import GiftIcon from "~icons/ri/gift-line";
  24. import CouponIcon from "~icons/ri/coupon-line";
  25. import MoneyIcon from "~icons/ri/money-cny-circle-line";
  26. echarts.use([
  27. BarChart,
  28. LineChart,
  29. PieChart,
  30. GridComponent,
  31. TooltipComponent,
  32. LegendComponent,
  33. TitleComponent,
  34. CanvasRenderer
  35. ]);
  36. defineOptions({
  37. name: "Dashboard"
  38. });
  39. const loading = ref(false);
  40. const overviewData = ref<Record<string, any>>({});
  41. const statisticsRawData = ref<Record<string, any>>({});
  42. const quickLinksRawData = ref<Record<string, any>>({});
  43. const deviceStatusRawList = ref<{ name: string; value: number; color: string }[]>([]);
  44. const statisticsType = ref("week");
  45. const statisticsData = computed(() => {
  46. const raw = statisticsRawData.value;
  47. const dates = raw.dates || [];
  48. const salesData = raw.salesData || [];
  49. const ordersData = raw.ordersData || [];
  50. return dates.map((date: string, index: number) => ({
  51. date,
  52. orders: ordersData[index] || 0,
  53. revenue: salesData[index] || 0,
  54. users: 0
  55. }));
  56. });
  57. const quickLinks = computed(() => {
  58. const raw = quickLinksRawData.value;
  59. return [
  60. { id: 1, title: "设备管理", icon: DeviceIcon, path: "/device/list", count: raw.needRestock || 0 },
  61. { id: 2, title: "订单管理", icon: OrderIcon, path: "/order/list", count: raw.unpaidOrders || 0 },
  62. { id: 3, title: "用户管理", icon: UserIcon, path: "/user/list" },
  63. { id: 4, title: "库存管理", icon: StackIcon, path: "/inventory/list" },
  64. { id: 5, title: "营销活动", icon: GiftIcon, path: "/marketing/activity" },
  65. { id: 6, title: "优惠券", icon: CouponIcon, path: "/marketing/coupon" }
  66. ];
  67. });
  68. const deviceStatusList = computed(() => {
  69. const list = deviceStatusRawList.value;
  70. if (!list || list.length === 0) return [];
  71. return list.map((item, index) => {
  72. const isOnline = item.name === "在线" || item.name?.includes("在线");
  73. return {
  74. deviceId: String(index + 1),
  75. deviceName: `设备 ${index + 1}`,
  76. status: isOnline ? 1 : 0
  77. };
  78. });
  79. });
  80. const deviceOnlineCount = computed(() => {
  81. return deviceStatusList.value.filter(d => d.status === 1).length;
  82. });
  83. const deviceOfflineCount = computed(() => {
  84. return deviceStatusList.value.filter(d => d.status === 0).length;
  85. });
  86. const formatMoney = (value: number) => {
  87. const num = Number(value) || 0;
  88. if (num >= 10000) {
  89. return (num / 10000).toFixed(2) + "万";
  90. }
  91. return num.toFixed(2);
  92. };
  93. const getValue = (key: string, isMoney: boolean = false) => {
  94. const value = overviewData.value?.[key] ?? 0;
  95. return isMoney ? formatMoney(value) : value;
  96. };
  97. const overviewCards = [
  98. { key: "totalUsers", label: "总用户数", icon: UserIcon, color: "#3B82F6", bgColor: "#EFF6FF", extraKey: "todayNewUsers", extraLabel: "今日新增" },
  99. { key: "totalOrders", label: "总订单数", icon: OrderIcon, color: "#8B5CF6", bgColor: "#F5F3FF", extraKey: "todayOrders", extraLabel: "今日订单" },
  100. { key: "totalRevenue", label: "总收入(元)", icon: MoneyIcon, color: "#10B981", bgColor: "#ECFDF5", extraKey: "todayRevenue", extraLabel: "今日收入", isMoney: true },
  101. { key: "totalDevices", label: "设备总数", icon: DeviceIcon, color: "#F59E0B", bgColor: "#FFFBEB", extraKey: "onlineDevices", extraLabel: "在线设备" }
  102. ];
  103. let orderChart: echarts.ECharts | null = null;
  104. let revenueChart: echarts.ECharts | null = null;
  105. let deviceChart: echarts.ECharts | null = null;
  106. const initCharts = () => {
  107. nextTick(() => {
  108. initOrderChart();
  109. initRevenueChart();
  110. initDeviceChart();
  111. });
  112. };
  113. const initOrderChart = () => {
  114. const chartDom = document.getElementById("order-chart");
  115. if (!chartDom) return;
  116. if (orderChart) {
  117. orderChart.dispose();
  118. }
  119. orderChart = echarts.init(chartDom);
  120. const option: echarts.EChartsOption = {
  121. tooltip: {
  122. trigger: "axis",
  123. backgroundColor: "rgba(255, 255, 255, 0.95)",
  124. borderColor: "#e5e7eb",
  125. borderWidth: 1,
  126. textStyle: { color: "#374151" }
  127. },
  128. grid: {
  129. left: "3%",
  130. right: "4%",
  131. bottom: "3%",
  132. top: "10%",
  133. containLabel: true
  134. },
  135. xAxis: {
  136. type: "category",
  137. data: statisticsData.value.map(item => item.date),
  138. axisLine: { lineStyle: { color: "#e5e7eb" } },
  139. axisLabel: { color: "#6b7280", fontSize: 11 }
  140. },
  141. yAxis: {
  142. type: "value",
  143. axisLine: { show: false },
  144. axisTick: { show: false },
  145. splitLine: { lineStyle: { color: "#f3f4f6" } },
  146. axisLabel: { color: "#6b7280", fontSize: 11 }
  147. },
  148. series: [{
  149. name: "订单数",
  150. type: "bar",
  151. data: statisticsData.value.map(item => item.orders),
  152. itemStyle: {
  153. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  154. { offset: 0, color: "#8B5CF6" },
  155. { offset: 1, color: "#A78BFA" }
  156. ]),
  157. borderRadius: [4, 4, 0, 0]
  158. },
  159. barWidth: 20
  160. }]
  161. };
  162. orderChart.setOption(option);
  163. };
  164. const initRevenueChart = () => {
  165. const chartDom = document.getElementById("revenue-chart");
  166. if (!chartDom) return;
  167. if (revenueChart) {
  168. revenueChart.dispose();
  169. }
  170. revenueChart = echarts.init(chartDom);
  171. const option: echarts.EChartsOption = {
  172. tooltip: {
  173. trigger: "axis",
  174. backgroundColor: "rgba(255, 255, 255, 0.95)",
  175. borderColor: "#e5e7eb",
  176. borderWidth: 1,
  177. textStyle: { color: "#374151" },
  178. formatter: (params: any) => {
  179. const data = params[0];
  180. return `${data.name}<br/>收入: ¥${formatMoney(data.value)}`;
  181. }
  182. },
  183. grid: {
  184. left: "3%",
  185. right: "4%",
  186. bottom: "3%",
  187. top: "10%",
  188. containLabel: true
  189. },
  190. xAxis: {
  191. type: "category",
  192. data: statisticsData.value.map(item => item.date),
  193. axisLine: { lineStyle: { color: "#e5e7eb" } },
  194. axisLabel: { color: "#6b7280", fontSize: 11 },
  195. boundaryGap: false
  196. },
  197. yAxis: {
  198. type: "value",
  199. axisLine: { show: false },
  200. axisTick: { show: false },
  201. splitLine: { lineStyle: { color: "#f3f4f6" } },
  202. axisLabel: { color: "#6b7280", fontSize: 11 }
  203. },
  204. series: [{
  205. name: "收入",
  206. type: "line",
  207. data: statisticsData.value.map(item => item.revenue),
  208. smooth: true,
  209. symbol: "circle",
  210. symbolSize: 6,
  211. lineStyle: { color: "#10B981", width: 2 },
  212. itemStyle: { color: "#10B981" },
  213. areaStyle: {
  214. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  215. { offset: 0, color: "rgba(16, 185, 129, 0.3)" },
  216. { offset: 1, color: "rgba(16, 185, 129, 0.05)" }
  217. ])
  218. }
  219. }]
  220. };
  221. revenueChart.setOption(option);
  222. };
  223. const initDeviceChart = () => {
  224. const chartDom = document.getElementById("device-chart");
  225. if (!chartDom) return;
  226. if (deviceChart) {
  227. deviceChart.dispose();
  228. }
  229. deviceChart = echarts.init(chartDom);
  230. const total = deviceOnlineCount.value + deviceOfflineCount.value;
  231. if (total === 0) {
  232. chartDom.style.display = "none";
  233. return;
  234. }
  235. const option: echarts.EChartsOption = {
  236. tooltip: {
  237. trigger: "item",
  238. backgroundColor: "rgba(255, 255, 255, 0.95)",
  239. borderColor: "#e5e7eb",
  240. borderWidth: 1,
  241. textStyle: { color: "#374151" },
  242. formatter: "{b}: {c}台 ({d}%)"
  243. },
  244. legend: {
  245. orient: "vertical",
  246. right: "5%",
  247. top: "center",
  248. textStyle: { color: "#6b7280", fontSize: 12 }
  249. },
  250. series: [{
  251. name: "设备状态",
  252. type: "pie",
  253. radius: ["50%", "70%"],
  254. center: ["35%", "50%"],
  255. avoidLabelOverlap: false,
  256. label: { show: false },
  257. emphasis: {
  258. label: { show: true, fontSize: 14, fontWeight: "bold" }
  259. },
  260. labelLine: { show: false },
  261. data: [
  262. { value: deviceOnlineCount.value, name: "在线", itemStyle: { color: "#10B981" } },
  263. { value: deviceOfflineCount.value, name: "离线", itemStyle: { color: "#EF4444" } }
  264. ]
  265. }]
  266. };
  267. deviceChart.setOption(option);
  268. };
  269. const fetchData = async () => {
  270. loading.value = true;
  271. try {
  272. const [overviewRes, statisticsRes, quickLinksRes, deviceRes] = await Promise.all([
  273. getOverview(),
  274. getStatistics(statisticsType.value),
  275. getQuickLinks(),
  276. getDeviceStatus()
  277. ]);
  278. overviewData.value = overviewRes.data || {};
  279. statisticsRawData.value = statisticsRes.data || {};
  280. quickLinksRawData.value = quickLinksRes.data || {};
  281. deviceStatusRawList.value = deviceRes.data || [];
  282. initCharts();
  283. } catch (error) {
  284. console.error("获取数据失败:", error);
  285. } finally {
  286. loading.value = false;
  287. }
  288. };
  289. const changeStatisticsType = (type: string) => {
  290. statisticsType.value = type;
  291. fetchData();
  292. };
  293. const refreshAll = () => {
  294. fetchData();
  295. };
  296. watch(() => statisticsData.value, () => {
  297. nextTick(() => {
  298. if (orderChart) initOrderChart();
  299. if (revenueChart) initRevenueChart();
  300. });
  301. }, { deep: true });
  302. onMounted(() => {
  303. fetchData();
  304. window.addEventListener("resize", () => {
  305. orderChart?.resize();
  306. revenueChart?.resize();
  307. deviceChart?.resize();
  308. });
  309. });
  310. </script>
  311. <template>
  312. <div class="dashboard-container">
  313. <div class="dashboard-header">
  314. <div class="header-left">
  315. <h1 class="page-title">数据概览</h1>
  316. <p class="page-subtitle">实时掌握业务运营状况</p>
  317. </div>
  318. <el-button type="primary" :loading="loading" @click="refreshAll">
  319. <el-icon><IconifyIconOffline :icon="RefreshIcon" /></el-icon>
  320. 刷新数据
  321. </el-button>
  322. </div>
  323. <div class="overview-cards">
  324. <div v-for="card in overviewCards" :key="card.key" class="stat-card">
  325. <div class="card-header">
  326. <div class="card-icon" :style="{ backgroundColor: card.bgColor, color: card.color }">
  327. <IconifyIconOffline :icon="card.icon" width="22" />
  328. </div>
  329. <div class="card-info">
  330. <div class="card-value">{{ getValue(card.key, card.isMoney) }}</div>
  331. <div class="card-label">{{ card.label }}</div>
  332. </div>
  333. </div>
  334. <div class="card-footer">
  335. <span class="footer-label">{{ card.extraLabel }}</span>
  336. <span class="footer-value" :style="{ color: card.color }">
  337. {{ getValue(card.extraKey, card.isMoney) }}
  338. </span>
  339. </div>
  340. </div>
  341. </div>
  342. <div class="quick-links-section">
  343. <div class="section-title">快捷入口</div>
  344. <div class="quick-links">
  345. <router-link v-for="link in quickLinks" :key="link.id" :to="link.path" class="quick-link-item">
  346. <div class="link-icon">
  347. <IconifyIconOffline :icon="link.icon" width="20" />
  348. </div>
  349. <span class="link-title">{{ link.title }}</span>
  350. <span v-if="link.count !== undefined && link.count > 0" class="link-badge">{{ link.count }}</span>
  351. </router-link>
  352. </div>
  353. </div>
  354. <div class="charts-row">
  355. <div class="chart-panel">
  356. <div class="panel-header">
  357. <span class="section-title">订单趋势</span>
  358. <div class="tab-buttons">
  359. <el-button :type="statisticsType === 'week' ? 'primary' : 'default'" size="small" @click="changeStatisticsType('week')">近7天</el-button>
  360. <el-button :type="statisticsType === 'month' ? 'primary' : 'default'" size="small" @click="changeStatisticsType('month')">近30天</el-button>
  361. <el-button :type="statisticsType === 'year' ? 'primary' : 'default'" size="small" @click="changeStatisticsType('year')">近一年</el-button>
  362. </div>
  363. </div>
  364. <div id="order-chart" class="chart-container"></div>
  365. </div>
  366. <div class="chart-panel">
  367. <div class="panel-header">
  368. <span class="section-title">收入趋势</span>
  369. </div>
  370. <div id="revenue-chart" class="chart-container"></div>
  371. </div>
  372. </div>
  373. <div class="bottom-row">
  374. <div class="chart-panel wide">
  375. <div class="panel-header">
  376. <span class="section-title">数据明细</span>
  377. </div>
  378. <el-table :data="statisticsData" style="width: 100%" size="small" max-height="300">
  379. <el-table-column prop="date" label="日期" width="100" />
  380. <el-table-column prop="orders" label="订单数" width="100">
  381. <template #default="{ row }">
  382. <span class="text-purple">{{ row.orders }}</span>
  383. </template>
  384. </el-table-column>
  385. <el-table-column prop="revenue" label="收入(元)">
  386. <template #default="{ row }">
  387. <span class="text-green">¥{{ formatMoney(row.revenue) }}</span>
  388. </template>
  389. </el-table-column>
  390. <el-table-column prop="users" label="新增用户" width="100">
  391. <template #default="{ row }">
  392. <span class="text-blue">{{ row.users }}</span>
  393. </template>
  394. </el-table-column>
  395. </el-table>
  396. </div>
  397. <div class="device-panel">
  398. <div class="panel-header">
  399. <span class="section-title">设备状态</span>
  400. <span class="device-count">{{ deviceStatusList.length }} 台设备</span>
  401. </div>
  402. <div class="device-content">
  403. <div id="device-chart" class="device-chart"></div>
  404. <div v-if="deviceStatusList.length === 0" class="empty-device">
  405. <el-empty description="暂无设备" :image-size="60" />
  406. </div>
  407. <div v-else class="device-stats">
  408. <div class="stat-item online">
  409. <span class="stat-dot"></span>
  410. <span class="stat-label">在线</span>
  411. <span class="stat-num">{{ deviceOnlineCount }}</span>
  412. </div>
  413. <div class="stat-item offline">
  414. <span class="stat-dot"></span>
  415. <span class="stat-label">离线</span>
  416. <span class="stat-num">{{ deviceOfflineCount }}</span>
  417. </div>
  418. </div>
  419. </div>
  420. </div>
  421. </div>
  422. </div>
  423. </template>
  424. <style lang="scss" scoped>
  425. .dashboard-container {
  426. padding: 20px;
  427. background: #f5f7fa;
  428. min-height: calc(100vh - 100px);
  429. }
  430. .dashboard-header {
  431. display: flex;
  432. justify-content: space-between;
  433. align-items: center;
  434. margin-bottom: 20px;
  435. }
  436. .header-left {
  437. .page-title {
  438. font-size: 22px;
  439. font-weight: 600;
  440. color: #303133;
  441. margin: 0 0 4px 0;
  442. }
  443. .page-subtitle {
  444. font-size: 13px;
  445. color: #909399;
  446. margin: 0;
  447. }
  448. }
  449. .overview-cards {
  450. display: flex;
  451. gap: 16px;
  452. margin-bottom: 20px;
  453. }
  454. .stat-card {
  455. flex: 1;
  456. background: #fff;
  457. border-radius: 8px;
  458. padding: 16px;
  459. box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
  460. }
  461. .card-header {
  462. display: flex;
  463. align-items: center;
  464. gap: 12px;
  465. margin-bottom: 12px;
  466. }
  467. .card-icon {
  468. width: 44px;
  469. height: 44px;
  470. border-radius: 8px;
  471. display: flex;
  472. align-items: center;
  473. justify-content: center;
  474. }
  475. .card-info {
  476. .card-value {
  477. font-size: 24px;
  478. font-weight: 700;
  479. color: #303133;
  480. line-height: 1.2;
  481. }
  482. .card-label {
  483. font-size: 13px;
  484. color: #909399;
  485. margin-top: 2px;
  486. }
  487. }
  488. .card-footer {
  489. padding-top: 12px;
  490. border-top: 1px solid #ebeef5;
  491. display: flex;
  492. align-items: center;
  493. gap: 8px;
  494. .footer-label {
  495. font-size: 12px;
  496. color: #909399;
  497. }
  498. .footer-value {
  499. font-size: 13px;
  500. font-weight: 600;
  501. }
  502. }
  503. .quick-links-section {
  504. background: #fff;
  505. border-radius: 8px;
  506. padding: 16px;
  507. margin-bottom: 20px;
  508. box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
  509. }
  510. .section-title {
  511. font-size: 15px;
  512. font-weight: 600;
  513. color: #303133;
  514. margin-bottom: 12px;
  515. }
  516. .quick-links {
  517. display: flex;
  518. gap: 12px;
  519. flex-wrap: wrap;
  520. }
  521. .quick-link-item {
  522. display: flex;
  523. align-items: center;
  524. gap: 8px;
  525. padding: 10px 16px;
  526. background: #f5f7fa;
  527. border-radius: 6px;
  528. text-decoration: none;
  529. color: #606266;
  530. transition: all 0.2s;
  531. min-width: 140px;
  532. &:hover {
  533. background: #e6e8eb;
  534. color: #409eff;
  535. }
  536. .link-icon {
  537. width: 32px;
  538. height: 32px;
  539. border-radius: 6px;
  540. background: #409eff;
  541. color: #fff;
  542. display: flex;
  543. align-items: center;
  544. justify-content: center;
  545. }
  546. .link-title {
  547. font-size: 14px;
  548. }
  549. .link-badge {
  550. background: #f56c6c;
  551. color: #fff;
  552. font-size: 11px;
  553. padding: 2px 6px;
  554. border-radius: 10px;
  555. min-width: 18px;
  556. text-align: center;
  557. }
  558. }
  559. .charts-row {
  560. display: flex;
  561. gap: 20px;
  562. margin-bottom: 20px;
  563. }
  564. .chart-panel {
  565. flex: 1;
  566. background: #fff;
  567. border-radius: 8px;
  568. padding: 16px;
  569. box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
  570. &.wide {
  571. flex: 2;
  572. }
  573. }
  574. .panel-header {
  575. display: flex;
  576. justify-content: space-between;
  577. align-items: center;
  578. margin-bottom: 12px;
  579. .section-title {
  580. margin-bottom: 0;
  581. }
  582. .device-count {
  583. font-size: 12px;
  584. color: #909399;
  585. background: #f5f7fa;
  586. padding: 4px 8px;
  587. border-radius: 4px;
  588. }
  589. }
  590. .tab-buttons {
  591. display: flex;
  592. gap: 8px;
  593. }
  594. .chart-container {
  595. height: 280px;
  596. width: 100%;
  597. }
  598. .bottom-row {
  599. display: flex;
  600. gap: 20px;
  601. }
  602. .device-panel {
  603. width: 320px;
  604. background: #fff;
  605. border-radius: 8px;
  606. padding: 16px;
  607. box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
  608. }
  609. .device-content {
  610. display: flex;
  611. flex-direction: column;
  612. align-items: center;
  613. }
  614. .device-chart {
  615. width: 100%;
  616. height: 180px;
  617. }
  618. .empty-device {
  619. width: 100%;
  620. display: flex;
  621. justify-content: center;
  622. }
  623. .device-stats {
  624. display: flex;
  625. justify-content: center;
  626. gap: 40px;
  627. margin-top: 16px;
  628. }
  629. .stat-item {
  630. display: flex;
  631. align-items: center;
  632. gap: 8px;
  633. .stat-dot {
  634. width: 10px;
  635. height: 10px;
  636. border-radius: 50%;
  637. }
  638. &.online .stat-dot {
  639. background: #10B981;
  640. }
  641. &.offline .stat-dot {
  642. background: #EF4444;
  643. }
  644. .stat-label {
  645. font-size: 13px;
  646. color: #606266;
  647. }
  648. .stat-num {
  649. font-size: 16px;
  650. font-weight: 600;
  651. color: #303133;
  652. }
  653. }
  654. .text-purple { color: #8B5CF6; }
  655. .text-green { color: #10B981; }
  656. .text-blue { color: #3B82F6; }
  657. @media screen and (max-width: 1400px) {
  658. .charts-row {
  659. flex-direction: column;
  660. }
  661. .bottom-row {
  662. flex-direction: column;
  663. }
  664. .device-panel {
  665. width: 100%;
  666. }
  667. }
  668. @media screen and (max-width: 1200px) {
  669. .overview-cards {
  670. flex-wrap: wrap;
  671. }
  672. .stat-card {
  673. flex: 1 1 calc(50% - 8px);
  674. min-width: 200px;
  675. }
  676. }
  677. @media screen and (max-width: 768px) {
  678. .dashboard-container {
  679. padding: 12px;
  680. }
  681. .overview-cards {
  682. flex-direction: column;
  683. }
  684. .stat-card {
  685. flex: 1 1 100%;
  686. }
  687. .quick-links {
  688. flex-direction: column;
  689. }
  690. .quick-link-item {
  691. width: 100%;
  692. }
  693. }
  694. </style>