iT邦幫忙

2025 iThome 鐵人賽

DAY 28
0
Vue.js

打造銷售系統30天修練 - 全集中・Vue之呼吸系列 第 28

Day 28:[Systemの呼吸・伍之型] 儀表板 - 數據視覺化與圖表

  • 分享至 

  • xImage
  •  

在過去的幾篇文章中,我們逐步構建了銷售系統的核心功能,我們的系統已經具備了處理日常營運的能力。然而,一個完善的商業系統不僅僅是處理交易和管理資料,更重要的是能夠從這些龐大的數據中提煉出有價值的資訊,輔助管理者做出明智的決策。

這正是「儀表板 (Dashboard)」的價值所在。儀表板是數據視覺化的核心應用,它將複雜的營運數據轉化為直觀易懂的圖表和指標,讓管理者能夠一目了然地掌握系統的脈動。

今天,我們將深入探討如何為 POS 系統設計和實作一個功能強大、視覺友善的儀表板,透過數據視覺化與圖表,讓數據說話。

儀表板的重要性與目標

一個設計良好的儀表板,對於 POS 系統的管理者來說,具有不可替代的重要性:

  • 快速概覽營運狀況:管理者無需深入查詢報表,即可在第一時間掌握關鍵營運指標,如總銷售額、訂單數量、熱銷商品等。
  • 趨勢分析與洞察:透過時間序列圖表,可以觀察銷售額、顧客數量的變化趨勢,發現季節性規律、促銷活動效果,或是潛在的營運問題。
  • 決策支援:提供客觀的數據依據,幫助管理者在商品採購、庫存管理、促銷策略、人員排班等方面做出更精準的決策。
  • 監控與預警:即時監控關鍵指標,一旦出現異常波動,可以及早發現並採取應對措施。
  • 績效評估:為員工或門店的績效評估提供量化數據。

我們的目標是構建一個能夠將複雜數據轉化為易於理解的視覺化呈現,並提供一定互動性的儀表板。

數據視覺化的基本原則

在設計儀表板和圖表時,我們需要遵循一些基本原則,以確保數據能夠被有效傳達:

  • 清晰性 (Clarity):圖表應簡潔明瞭,避免過度裝飾和不必要的資訊,確保核心訊息一目了然。
  • 準確性 (Accuracy):數據呈現必須真實可靠,圖表的比例、刻度應正確反映數據關係。
  • 相關性 (Relevance):只展示與業務目標和使用者決策相關的數據,避免資訊過載。
  • 一致性 (Consistency):圖表風格、顏色、字體、佈局應在整個儀表板中保持一致,提升使用者體驗。
  • 互動性 (Interactivity):適當提供篩選、鑽取 (Drill-down)、時間範圍選擇等功能,讓使用者能根據需求深入探索數據。

POS 系統儀表板常見的數據指標與圖表類型

針對 POS 系統的特性,儀表板通常會包含以下幾類關鍵數據指標和對應的圖表類型:

1. 銷售額相關

  • 總銷售額 (Total Sales)
    • 圖表類型:數字卡片 (Number Card)。
    • 用途:最直觀的營運成果展示。
  • 銷售額趨勢 (Sales Trend)
    • 圖表類型:折線圖 (Line Chart)。
    • 用途:按日、週、月、年展示銷售額變化,觀察增長或下降趨勢。
  • 銷售額分佈 (Sales Distribution)
    • 圖表類型:圓餅圖 (Pie Chart) 或柱狀圖 (Bar Chart)。
    • 用途:按商品類別、支付方式、銷售時段、門店等維度分析銷售額佔比。

2. 商品相關

  • 熱銷商品 (Top Selling Products)
    • 圖表類型:水平條形圖 (Horizontal Bar Chart)。
    • 用途:顯示銷量或銷售額前 N 名的商品,幫助優化庫存和採購。
  • 庫存預警 (Inventory Alert)
    • 圖表類型:列表 (Table) 或數字卡片。
    • 用途:顯示庫存量低於安全閾值的商品,提醒及時補貨。

3. 訂單相關

  • 訂單數量 (Total Orders)
    • 圖表類型:數字卡片。
    • 用途:顯示總訂單數或特定時間範圍內的訂單數。
  • 訂單狀態分佈 (Order Status Distribution)
    • 圖表類型:圓餅圖。
    • 用途:顯示「待付款」、「已付款」、「處理中」、「已完成」等各狀態訂單的比例。

4. 顧客相關

  • 新會員數量 (New Members)
    • 圖表類型:折線圖。
    • 用途:追蹤新會員的增長趨勢。
  • 活躍會員數 (Active Members)
    • 圖表類型:數字卡片。
    • 用途:顯示在特定時間內有消費行為的會員數量。

前端數據視覺化工具選型

在 Vue.js 專案中,有許多優秀的數據視覺化庫可以選擇:

  • Chart.js
    • 優點:輕量級、易於學習和使用、支援多種基本圖表類型、社區活躍。
    • 缺點:對於複雜的互動和大型數據集可能功能有限。
    • Vue.js 整合vue-chartjs 提供了方便的 Vue 組件封裝。
  • Apache ECharts
    • 優點:功能非常強大、圖表類型豐富、互動性強、性能優異、支援大數據量。
    • 缺點:相對 Chart.js 學習曲線稍高。
    • Vue.js 整合vue-echarts 提供了方便的 Vue 組件封裝。
  • D3.js (Data-Driven Documents)
    • 優點:極致的靈活性和客製化能力,可以創建任何你想要的視覺化效果。
    • 缺點:學習曲線陡峭,需要較強的 JavaScript 和 SVG 知識,通常用於高度客製化的需求。

選型考量:對於大多數 POS 系統的儀表板需求,Chart.js 或 ECharts 都是非常好的選擇。如果追求快速開發和基本功能,Chart.js 是一個不錯的起點;如果需要更豐富的圖表類型、更強大的互動性和處理大數據的能力,ECharts 會是更好的選擇。

以 Vue.js + Chart.js 為例

我們將以 Vue.js 結合 vue-chartjs 來實作儀表板。

  1. 數據準備

    • 後端 API 需要提供儀表板所需的聚合數據,例如按日期統計的銷售額、按類別統計的銷售額等。
    • 這些 API 應該支援時間範圍篩選等參數。
  2. Pinia Store (stores/dashboard.js)

    • 管理儀表板數據的狀態,包括各個圖表的數據、載入狀態、錯誤訊息。
    • 管理篩選條件(例如日期範圍選擇器)。
    • 封裝與後端儀表板 API 互動的邏輯,例如 fetchSalesData(), fetchProductCategorySales(), fetchOrderStats()
  3. 組件化

    • 將每個圖表或數據卡片設計成獨立的 Vue 組件,例如 SalesTrendChart.vue (折線圖)、CategorySalesPie.vue (圓餅圖)、TotalSalesCard.vue (數字卡片)。
    • 這些組件將接收來自父組件或 Pinia Store 的數據作為 props。
  4. 動態數據綁定

    • 在儀表板頁面 (DashBoard.vue) 中,從 useDashboardStore 獲取數據。
    • 將這些數據綁定到各個圖表組件的 props 上。
    • 當 Pinia Store 中的數據更新時,圖表會自動重新渲染。
  5. 互動性

    • 在儀表板頁面中加入日期範圍選擇器、篩選條件等 UI 元素。
    • 當使用者改變篩選條件時,觸發 useDashboardStore 中的 action,更新篩選參數並重新呼叫後端 API 獲取數據。
    • Pinia Store 獲取新數據後,會更新狀態,進而觸發圖表組件的重新渲染。

程式碼範例

<!-- src/components/SalesTrendChart.vue (使用 vue-chartjs 的折線圖組件) -->
<template>
  <Line :data="chartData" :options="chartOptions" />
</template>

<script setup>
import { Line } from 'vue-chartjs';
import { Chart as ChartJS, Title, Tooltip, Legend, LineElement, CategoryScale, LinearScale, PointElement } from 'chart.js';

ChartJS.register(Title, Tooltip, Legend, LineElement, CategoryScale, LinearScale, PointElement);

const props = defineProps({
  salesData: {
    type: Array,
    required: true,
  },
  labels: {
    type: Array,
    required: true,
  },
});

const chartData = computed(() => ({
  labels: props.labels,
  datasets: [
    {
      label: '每日銷售額',
      backgroundColor: '#42A5F5',
      borderColor: '#42A5F5',
      data: props.salesData,
      tension: 0.3, // 平滑曲線
    },
  ],
}));

const chartOptions = {
  responsive: true,
  maintainAspectRatio: false,
  plugins: {
    legend: {
      display: true,
    },
    title: {
      display: true,
      text: '銷售額趨勢',
    },
  },
  scales: {
    y: {
      beginAtZero: true,
      title: {
        display: true,
        text: '銷售額 (NTD)',
      },
    },
    x: {
      title: {
        display: true,
        text: '日期',
      },
    },
  },
};
</script>
// src/stores/dashboard.js 
import { defineStore } from 'pinia';
import api from '@/utils/api'; // 假設有一個 API 服務

export const useDashboardStore = defineStore('dashboard', {
  state: () => ({
    totalSales: 0,
    salesTrend: {
      labels: [],
      data: [],
    },
    categorySales: [],
    loading: false,
    error: null,
    dateRange: {
      startDate: new Date(new Date().setDate(new Date().getDate() - 7)).toISOString().split('T')[0],
      endDate: new Date().toISOString().split('T')[0],
    },
  }),
  actions: {
    async fetchDashboardData() {
      this.loading = true;
      try {
        // 獲取總銷售額
        const salesResponse = await api.get('/dashboard/total-sales', { params: this.dateRange });
        this.totalSales = salesResponse.data.totalSales;

        // 獲取銷售額趨勢
        const trendResponse = await api.get('/dashboard/sales-trend', { params: this.dateRange });
        this.salesTrend.labels = trendResponse.data.labels;
        this.salesTrend.data = trendResponse.data.data;

        // 獲取商品類別銷售分佈
        const categoryResponse = await api.get('/dashboard/category-sales', { params: this.dateRange });
        this.categorySales = categoryResponse.data.categorySales;

      } catch (err) {
        this.error = err;
        console.error('Error fetching dashboard data:', err);
      } finally {
        this.loading = false;
      }
    },
    setDateRange(newRange) {
      this.dateRange = newRange;
      this.fetchDashboardData(); // 日期範圍改變時重新獲取數據
    },
  },
});
<!-- src/view/DashBoard.vue (儀表板主頁面) -->
<template>
  <div class="dashboard p-4">
    <h1 class="text-2xl font-bold mb-4">儀表板</h1>

    <!-- 日期範圍選擇器 -->
    <div class="mb-6 flex items-center space-x-4">
      <label for="startDate">開始日期:</label>
      <input type="date" id="startDate" v-model="dashboardStore.dateRange.startDate" @change="dashboardStore.fetchDashboardData" class="border p-2 rounded">
      <label for="endDate">結束日期:</label>
      <input type="date" id="endDate" v-model="dashboardStore.dateRange.endDate" @change="dashboardStore.fetchDashboardData" class="border p-2 rounded">
    </div>

    <div v-if="dashboardStore.loading" class="text-center text-gray-500">載入數據中...</div>
    <div v-else-if="dashboardStore.error" class="text-center text-red-500">載入數據失敗: {{ dashboardStore.error.message }}</div>
    <div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
      <!-- 總銷售額卡片 -->
      <BaseCard class="col-span-1">
        <h2 class="text-xl font-semibold mb-2">總銷售額</h2>
        <p class="text-4xl font-bold text-green-600">NT$ {{ dashboardStore.totalSales.toLocaleString() }}</p>
      </BaseCard>

      <!-- 銷售額趨勢圖 -->
      <BaseCard class="md:col-span-2 lg:col-span-2">
        <SalesTrendChart :salesData="dashboardStore.salesTrend.data" :labels="dashboardStore.salesTrend.labels" class="h-80" />
      </BaseCard>

      <!-- 商品類別銷售分佈圖 -->
      <BaseCard class="col-span-1">
        <h2 class="text-xl font-semibold mb-2">商品類別銷售分佈</h2>
        <!-- 假設 CategorySalesPie 是一個圓餅圖組件 -->
        <!-- <CategorySalesPie :data="dashboardStore.categorySales" class="h-80" /> -->
        <p>圓餅圖將在此處顯示</p>
      </BaseCard>

      <!-- 其他數據卡片或圖表 -->
      <!-- <DashboardTodayStatsCard /> -->
      <!-- <DashboardPendingListCard /> -->
      <!-- <DashboardQuickSearchCard /> -->
    </div>
  </div>
</template>

<script setup>
import { onMounted } from 'vue';
import { useDashboardStore } from '@/stores/dashboard';
import BaseCard from '@/components/BaseCard.vue';
import SalesTrendChart from '@/components/SalesTrendChart.vue';
// import CategorySalesPie from '@/components/CategorySalesPie.vue'; // 假設有這個組件

const dashboardStore = useDashboardStore();

onMounted(() => {
  dashboardStore.fetchDashboardData();
});
</script>

總結

今天,我們深入探討了 POS 系統儀表板的設計與數據視覺化。
透過理解儀表板的重要性、遵循數據視覺化原則,並選擇合適的前端圖表庫,我們能夠將複雜的營運數據轉化為直觀、有洞察力的視覺呈現。結合 Vue.js 的組件化和 Pinia 的狀態管理,我們可以構建出一個高效且互動性強的儀表板。

明日,Day 29:[Systemの呼吸・陸之型] 響應式設計 - 多裝置適配。心を燃やせ 🔥!


上一篇
Day 27:[Systemの呼吸・肆之型] 訂單流程 - 狀態機與流程設計
下一篇
Day 29:[Systemの呼吸・陸之型] 響應式設計 - 多裝置適配
系列文
打造銷售系統30天修練 - 全集中・Vue之呼吸29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言