在過去的幾篇文章中,我們逐步構建了銷售系統的核心功能,我們的系統已經具備了處理日常營運的能力。然而,一個完善的商業系統不僅僅是處理交易和管理資料,更重要的是能夠從這些龐大的數據中提煉出有價值的資訊,輔助管理者做出明智的決策。
這正是「儀表板 (Dashboard)」的價值所在。儀表板是數據視覺化的核心應用,它將複雜的營運數據轉化為直觀易懂的圖表和指標,讓管理者能夠一目了然地掌握系統的脈動。
今天,我們將深入探討如何為 POS 系統設計和實作一個功能強大、視覺友善的儀表板,透過數據視覺化與圖表,讓數據說話。
一個設計良好的儀表板,對於 POS 系統的管理者來說,具有不可替代的重要性:
我們的目標是構建一個能夠將複雜數據轉化為易於理解的視覺化呈現,並提供一定互動性的儀表板。
在設計儀表板和圖表時,我們需要遵循一些基本原則,以確保數據能夠被有效傳達:
針對 POS 系統的特性,儀表板通常會包含以下幾類關鍵數據指標和對應的圖表類型:
在 Vue.js 專案中,有許多優秀的數據視覺化庫可以選擇:
vue-chartjs
提供了方便的 Vue 組件封裝。vue-echarts
提供了方便的 Vue 組件封裝。選型考量:對於大多數 POS 系統的儀表板需求,Chart.js 或 ECharts 都是非常好的選擇。如果追求快速開發和基本功能,Chart.js 是一個不錯的起點;如果需要更豐富的圖表類型、更強大的互動性和處理大數據的能力,ECharts 會是更好的選擇。
我們將以 Vue.js 結合 vue-chartjs
來實作儀表板。
數據準備:
Pinia Store (stores/dashboard.js
):
fetchSalesData()
, fetchProductCategorySales()
, fetchOrderStats()
。組件化:
SalesTrendChart.vue
(折線圖)、CategorySalesPie.vue
(圓餅圖)、TotalSalesCard.vue
(數字卡片)。動態數據綁定:
DashBoard.vue
) 中,從 useDashboardStore
獲取數據。互動性:
useDashboardStore
中的 action,更新篩選參數並重新呼叫後端 API 獲取數據。<!-- 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の呼吸・陸之型] 響應式設計 - 多裝置適配。心を燃やせ 🔥!