feat: build UI

This commit is contained in:
Viet An
2026-04-09 17:20:47 +07:00
parent bcfda00993
commit 631527225e
36 changed files with 11305 additions and 1123 deletions

View 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>

View File

@@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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 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>