feat: build UI
This commit is contained in:
27
app/components/dashboard/AvatarBox.vue
Normal file
27
app/components/dashboard/AvatarBox.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div
|
||||
class="fs-13 font-semibold mx-0 px-0 is-flex is-justify-content-center is-align-items-center is-flex-shrink-0 rounded-full size-10"
|
||||
style="border: 1px solid var(--bulma-grey-80)"
|
||||
:style="image && 'border: none'">
|
||||
<div>
|
||||
<span>{{text}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['text', 'image', 'type', 'size']
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.cbox {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--grey-100);
|
||||
border: 1px solid hsl(0, 0%, 85%);
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,90 @@
|
||||
<script setup>
|
||||
import DashboardHighlightCard from '@/components/dashboard/DashboardHighlightCard.vue';
|
||||
import Delivery from '@/components/dashboard/Delivery.vue';
|
||||
import OrderStatus from '@/components/dashboard/OrderStatus.vue';
|
||||
import RevenueChart from '@/components/dashboard/RevenueChart.vue';
|
||||
import TopCustomers from '@/components/dashboard/TopCustomers.vue';
|
||||
import TopProducts from '@/components/dashboard/TopProducts.vue';
|
||||
import Warnings from '@/components/dashboard/Warnings.vue';
|
||||
|
||||
const highlights = [
|
||||
{
|
||||
name: 'Doanh thu hôm nay',
|
||||
value: '72.5M',
|
||||
color: 'blue',
|
||||
icon: 'material-symbols:attach-money-rounded',
|
||||
subheader: {
|
||||
value: '+12.5%',
|
||||
fluctuation: 'up',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Số đơn hàng',
|
||||
value: '73',
|
||||
color: 'purple',
|
||||
icon: 'material-symbols:shopping-cart-outline-rounded',
|
||||
subheader: {
|
||||
value: '+8 đơn',
|
||||
fluctuation: 'up',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Đơn đang giao',
|
||||
value: '8',
|
||||
color: 'orange',
|
||||
icon: 'material-symbols:delivery-truck-speed-outline-rounded',
|
||||
},
|
||||
{
|
||||
name: 'Đơn hoàn thành',
|
||||
value: '38',
|
||||
color: 'green',
|
||||
icon: 'material-symbols:check-circle-outline-rounded',
|
||||
},
|
||||
{
|
||||
name: 'Công nợ phải thu',
|
||||
value: '245M',
|
||||
color: 'red',
|
||||
icon: 'material-symbols:universal-currency-alt-outline-rounded',
|
||||
subheader: {
|
||||
value: '+15M',
|
||||
fluctuation: 'down',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Tồn kho',
|
||||
value: '1,250',
|
||||
color: 'cyan',
|
||||
icon: 'material-symbols:box-outline-rounded',
|
||||
subheader: {
|
||||
value: '-45 SP',
|
||||
fluctuation: 'down',
|
||||
}
|
||||
},
|
||||
]
|
||||
</script>
|
||||
<template>
|
||||
Dashboard
|
||||
<div>
|
||||
<div class="fixed-grid has-2-cols has-3-cols-tablet has-6-cols-desktop">
|
||||
<div class="grid">
|
||||
<DashboardHighlightCard
|
||||
v-for="highlight in highlights"
|
||||
v-bind="highlight"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fixed-grid has-3-cols">
|
||||
<div class="grid">
|
||||
<div class="cell is-col-span-2">
|
||||
<RevenueChart />
|
||||
</div>
|
||||
<div class="cell">
|
||||
<TopCustomers />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TopProducts />
|
||||
<OrderStatus />
|
||||
<Delivery />
|
||||
<Warnings />
|
||||
</div>
|
||||
</template>
|
||||
41
app/components/dashboard/DashboardHighlightCard.vue
Normal file
41
app/components/dashboard/DashboardHighlightCard.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
name: String,
|
||||
value: String,
|
||||
color: String,
|
||||
icon: String,
|
||||
subheader: Object
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div class="cell">
|
||||
<div class="card h-full">
|
||||
<div class="card-content is-flex is-gap-0.5 is-justify-content-space-between">
|
||||
<div>
|
||||
<p class="fs-14 has-text-grey mb-1">{{ name }}</p>
|
||||
<p class="fsb-26 mb-1 has-text-black">{{ value }}</p>
|
||||
<div v-if="subheader"
|
||||
:class="[
|
||||
'is-flex is-gap-0.5 is-align-items-center',
|
||||
subheader.fluctuation === 'up' ? 'has-text-green-40' : 'has-text-red-40'
|
||||
]"
|
||||
>
|
||||
<Icon
|
||||
:name="subheader.fluctuation === 'up' ? 'material-symbols:arrow-upward-rounded' :'material-symbols:arrow-downward-rounded'"
|
||||
color="inherit"
|
||||
/>
|
||||
<p class="fs-14">{{ subheader.value }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
'rounded-lg size-12 is-flex-shrink-0 is-flex is-justify-content-center is-align-items-center',
|
||||
`has-background-${color}-soft has-text-${color}-40`
|
||||
]"
|
||||
">
|
||||
<Icon :name="icon" :size="28" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
53
app/components/dashboard/Delivery.vue
Normal file
53
app/components/dashboard/Delivery.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup>
|
||||
import Driver from '@/components/dashboard/Driver.vue';
|
||||
|
||||
const drivers = [
|
||||
{
|
||||
name: 'Nguyễn Văn A',
|
||||
status: 'Đang giao',
|
||||
deliveries: 3,
|
||||
deliveries_completed: 2,
|
||||
},
|
||||
{
|
||||
name: 'Trần Văn B',
|
||||
status: 'Đang giao',
|
||||
deliveries: 2,
|
||||
deliveries_completed: 1,
|
||||
},
|
||||
{
|
||||
name: 'Lê Thị C',
|
||||
status: 'Hoàn thành',
|
||||
deliveries: 4,
|
||||
deliveries_completed: 4,
|
||||
},
|
||||
{
|
||||
name: 'Phạm Văn D',
|
||||
status: 'Đang giao',
|
||||
deliveries: 1,
|
||||
deliveries_completed: 0,
|
||||
},
|
||||
]
|
||||
</script>
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<p class="fs-17 font-semibold mb-4">Giao nhận & Tài xế</p>
|
||||
<div class="fixed-grid has-3-cols">
|
||||
<div class="grid">
|
||||
<div class="cell is-col-span-2">
|
||||
<div style="border-radius: 0.5rem; overflow: hidden">
|
||||
<NuxtImg src="/map-demo.png" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell is-flex is-flex-direction-column is-gap-2">
|
||||
<Driver
|
||||
v-for="driver in drivers"
|
||||
:key="driver.name"
|
||||
v-bind="driver"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
35
app/components/dashboard/Driver.vue
Normal file
35
app/components/dashboard/Driver.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup>
|
||||
import AvatarBox from '@/components/dashboard/AvatarBox.vue';
|
||||
|
||||
const props = defineProps({
|
||||
name: String,
|
||||
status: String,
|
||||
deliveries: Number,
|
||||
deliveries_completed: Number,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="is-flex is-gap-2 fs-14 p-3 rounded-lg"
|
||||
:style="{
|
||||
border: '1px solid var(--bulma-grey-80)',
|
||||
}"
|
||||
>
|
||||
<AvatarBox :text="name.slice(0, 2)" />
|
||||
<div class="is-flex-grow-1">
|
||||
<div class="is-flex is-gap-1 is-align-items-center">
|
||||
<p>{{ name }}</p>
|
||||
<span :class="['tag', status === 'Đang giao' ? 'is-warning' : 'is-success']">{{ status }}</span>
|
||||
</div>
|
||||
<p class="fs-13 has-text-grey">Đơn: {{ deliveries_completed }}/{{ deliveries }}</p>
|
||||
<progress
|
||||
v-if="deliveries !== deliveries_completed"
|
||||
class="progress is-small is-primary mt-2"
|
||||
style="--bulma-size-small: 0.4rem;"
|
||||
:value="deliveries_completed"
|
||||
:max="deliveries"
|
||||
>
|
||||
</progress>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
54
app/components/dashboard/OrderStatus.vue
Normal file
54
app/components/dashboard/OrderStatus.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<script setup>
|
||||
import OrderStatusCard from '@/components/dashboard/OrderStatusCard.vue';
|
||||
|
||||
const statuses = [
|
||||
{
|
||||
id: 1,
|
||||
code: 'pending',
|
||||
name: 'Chờ xử lý',
|
||||
value: 12,
|
||||
color: 'orange',
|
||||
icon: 'material-symbols:clock-loader-40'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
code: 'delivering',
|
||||
name: 'Đang giao',
|
||||
value: 8,
|
||||
color: 'blue',
|
||||
icon: 'material-symbols:delivery-truck-speed-outline-rounded'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
code: 'delivered',
|
||||
name: 'Đã giao',
|
||||
value: 15,
|
||||
color: 'purple',
|
||||
icon: 'material-symbols:bucket-check-outline-rounded'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
code: 'completed',
|
||||
name: 'Hoàn thành',
|
||||
value: 38,
|
||||
color: 'green',
|
||||
icon: 'material-symbols:check-circle-outline-rounded'
|
||||
},
|
||||
]
|
||||
</script>
|
||||
<template>
|
||||
<div class="card h-full">
|
||||
<div class="card-content">
|
||||
<p class="fs-17 font-semibold mb-4">Trạng thái đơn hàng</p>
|
||||
<div class="fixed-grid has-2-cols-mobile has-4-cols">
|
||||
<div class="grid">
|
||||
<OrderStatusCard
|
||||
v-for="status in statuses"
|
||||
:key="status.name"
|
||||
v-bind="status"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
33
app/components/dashboard/OrderStatusCard.vue
Normal file
33
app/components/dashboard/OrderStatusCard.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
id: Number,
|
||||
code: String,
|
||||
name: String,
|
||||
value: Number,
|
||||
icon: String,
|
||||
color: String,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div class="cell">
|
||||
<div class="card" :style="{ border: `1px solid var(--bulma-${color}-70)` }">
|
||||
<div class="card-content is-flex is-flex-direction-column is-align-items-center is-gap-1">
|
||||
<div
|
||||
:class="[
|
||||
'p-3 is-flex rounded-full',
|
||||
`has-background-${color}-90`,
|
||||
`has-text-${color}-40`,
|
||||
]" >
|
||||
<Icon
|
||||
:name="icon"
|
||||
:size="24"
|
||||
/>
|
||||
</div>
|
||||
<div class="is-flex is-flex-direction-column is-align-items-center">
|
||||
<p class="fs-24 font-bold">{{ value }}</p>
|
||||
<p class="has-text-grey">{{ name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
61
app/components/dashboard/RevenueChart.vue
Normal file
61
app/components/dashboard/RevenueChart.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<script setup>
|
||||
const { $shortenCurrency } = useNuxtApp();
|
||||
const revenueChartOptions = {
|
||||
chart: {
|
||||
type: 'spline'
|
||||
},
|
||||
credits: {
|
||||
enabled: false,
|
||||
},
|
||||
title: {
|
||||
text: null,
|
||||
},
|
||||
xAxis: {
|
||||
categories: [
|
||||
'10/4', '11/4', '12/4', '13/4', '14/4', '15/4', '16/4', '17/4', '18/4'
|
||||
],
|
||||
accessibility: {
|
||||
description: 'Dates'
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
title: {
|
||||
text: 'Doanh thu'
|
||||
},
|
||||
labels: {
|
||||
format: '{value}'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
crosshairs: true,
|
||||
shared: true,
|
||||
valueSuffix: ' VNĐ',
|
||||
},
|
||||
plotOptions: {
|
||||
spline: {
|
||||
marker: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
name: 'Doanh thu',
|
||||
data: [45000000, 52000000, 48000000, 51000000, 58000000, 61000000, 67500000, 72000000, 69000000],
|
||||
showInLegend: false,
|
||||
}]
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="is-flex is-justify-content-space-between mb-2">
|
||||
<p style="font-weight: 600; font-size: 18px">Doanh thu theo ngày</p>
|
||||
<div class="buttons">
|
||||
<button class="button is-primary fs-14">7 ngày</button>
|
||||
<button class="button is-white fs-14">30 ngày</button>
|
||||
</div>
|
||||
</div>
|
||||
<highcharts :options="revenueChartOptions" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
23
app/components/dashboard/TopCustomer.vue
Normal file
23
app/components/dashboard/TopCustomer.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup>
|
||||
import AvatarBox from '@/components/dashboard/AvatarBox.vue';
|
||||
|
||||
const props = defineProps({
|
||||
name: String,
|
||||
order_count: Number,
|
||||
paid: Number,
|
||||
})
|
||||
|
||||
const { $shortenCurrency } = useNuxtApp();
|
||||
</script>
|
||||
<template>
|
||||
<div class="is-flex is-gap-1 is-justify-content-space-between">
|
||||
<div class="is-flex is-gap-1">
|
||||
<AvatarBox :text="name.slice(0, 2)" />
|
||||
<div>
|
||||
<p>{{ name }}</p>
|
||||
<p class="fs-13 has-text-grey">{{ order_count }} đơn hàng</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="font-semibold">{{ $shortenCurrency(paid) }}</p>
|
||||
</div>
|
||||
</template>
|
||||
47
app/components/dashboard/TopCustomers.vue
Normal file
47
app/components/dashboard/TopCustomers.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script setup>
|
||||
import TopCustomer from '@/components/dashboard/TopCustomer.vue';
|
||||
|
||||
|
||||
const customers = [
|
||||
{
|
||||
name: 'Công ty TNHH ABC',
|
||||
order_count: 45,
|
||||
paid: 125000000,
|
||||
},
|
||||
{
|
||||
name: 'Siêu thị XYZ',
|
||||
order_count: 38,
|
||||
paid: 98000000,
|
||||
},
|
||||
{
|
||||
name: 'Nhà hàng Đông Dương',
|
||||
order_count: 32,
|
||||
paid: 87000000,
|
||||
},
|
||||
{
|
||||
name: 'Khách sạn Mường Thanh',
|
||||
order_count: 28,
|
||||
paid: 76000000,
|
||||
},
|
||||
{
|
||||
name: 'Cửa hàng Bách Hoá',
|
||||
order_count: 24,
|
||||
paid: 64000000,
|
||||
},
|
||||
|
||||
]
|
||||
</script>
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<p class="fs-17 font-semibold mb-4">Top khách hàng</p>
|
||||
<div class="is-flex is-flex-direction-column is-gap-1.5">
|
||||
<TopCustomer
|
||||
v-for="cus in customers"
|
||||
:key="cus.name"
|
||||
v-bind="cus"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
29
app/components/dashboard/TopProduct.vue
Normal file
29
app/components/dashboard/TopProduct.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
name: String,
|
||||
sold_count: Number,
|
||||
revenue: Number,
|
||||
topRevenue: Number,
|
||||
});
|
||||
|
||||
const { $shortenCurrency } = useNuxtApp();
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div class="is-flex is-gap-1 is-justify-content-space-between mb-2">
|
||||
<div>
|
||||
<p>{{ name }}</p>
|
||||
<p class="fs-13 has-text-grey-60">Đã bán {{ sold_count }} sản phẩm</p>
|
||||
</div>
|
||||
<p class="font-semibold">{{ $shortenCurrency(revenue) }}</p>
|
||||
</div>
|
||||
<progress class="progress is-small is-primary" :value="revenue" :max="topRevenue">
|
||||
15%
|
||||
</progress>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.progress {
|
||||
--bulma-size-small: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
48
app/components/dashboard/TopProducts.vue
Normal file
48
app/components/dashboard/TopProducts.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script setup>
|
||||
import TopProduct from '@/components/dashboard/TopProduct.vue';
|
||||
|
||||
const products = [
|
||||
{
|
||||
name: 'Gạo ST25 - Bao 5kg',
|
||||
sold_count: 1250,
|
||||
revenue: 156000000
|
||||
},
|
||||
{
|
||||
name: 'Nước mắm Phú Quốc - Chai 500ml',
|
||||
sold_count: 980,
|
||||
revenue: 132000000
|
||||
},
|
||||
{
|
||||
name: 'Đường tinh luyện - Bao 1kg',
|
||||
sold_count: 856,
|
||||
revenue: 98000000
|
||||
},
|
||||
{
|
||||
name: 'Dầu ăn Neptune - Chai 1L',
|
||||
sold_count: 742,
|
||||
revenue: 87000000
|
||||
},
|
||||
{
|
||||
name: 'Bột mì đa dụng - Bao 1kg',
|
||||
sold_count: 623,
|
||||
revenue: 72000000
|
||||
},
|
||||
]
|
||||
</script>
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<p class="fs-17 font-semibold mb-4">Top sản phẩm</p>
|
||||
<div class="is-flex is-flex-direction-column is-gap-2">
|
||||
<TopProduct
|
||||
v-for="product in products"
|
||||
:key="product.name"
|
||||
v-bind="{
|
||||
...product,
|
||||
topRevenue: products[0].revenue
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
36
app/components/dashboard/Warning.vue
Normal file
36
app/components/dashboard/Warning.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
name: String,
|
||||
details: String,
|
||||
level: Number,
|
||||
type: String
|
||||
})
|
||||
|
||||
const color = computed(() => {
|
||||
if (props.level === 1) return 'yellow';
|
||||
if (props.level === 2) return 'orange';
|
||||
if (props.level === 3) return 'red';
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="['card m-0', `has-background-${color}-95`]"
|
||||
:style="{
|
||||
borderColor: `var(--bulma-${color}-70)`,
|
||||
}"
|
||||
>
|
||||
<div class="card-content p-4 is-flex is-align-items-center is-gap-2">
|
||||
<Icon
|
||||
:name="type === 'time' ? 'material-symbols:clock-loader-40' : 'material-symbols:box-outline-rounded'"
|
||||
:size="20"
|
||||
:class="`has-text-${color}-45`"
|
||||
/>
|
||||
<div>
|
||||
<p :class="`has-text-${color}-40`">{{ name }}</p>
|
||||
<p class="fs-13 has-text-grey">{{ details }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
38
app/components/dashboard/Warnings.vue
Normal file
38
app/components/dashboard/Warnings.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup>
|
||||
import Warning from '@/components/dashboard/Warning.vue';
|
||||
|
||||
const warnings = [
|
||||
{
|
||||
name: 'Công nợ sắp đến hạn',
|
||||
details: 'Công ty TNHH ABC - 35.000.000đ - Hạn: 25/03/2026',
|
||||
level: 1,
|
||||
type: 'time',
|
||||
},
|
||||
{
|
||||
name: 'Đơn giao trễ',
|
||||
details: 'Đơn hàng #DH-2156 - Đã trễ 2 giờ - Khách: Siêu thị XYZ',
|
||||
level: 3,
|
||||
type: 'time',
|
||||
},
|
||||
{
|
||||
name: 'Tồn kho thấp',
|
||||
details: 'Gạo ST25 - Chỉ còn 45 bao - Cần nhập thêm',
|
||||
level: 2,
|
||||
type: 'inventory'
|
||||
},
|
||||
]
|
||||
</script>
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<p class="fs-17 font-semibold mb-4">Cảnh báo và thông báo</p>
|
||||
<div class="is-flex is-flex-direction-column is-gap-1">
|
||||
<Warning
|
||||
v-for="warning in warnings"
|
||||
:key="warning.details"
|
||||
v-bind="warning"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user