From 28b0a4be38081747c3e99beb574bbbabaebc7375 Mon Sep 17 00:00:00 2001 From: "s.esmaeili" Date: Tue, 13 Oct 2026 08:53:25 +0330 Subject: [PATCH] ui(admin-panel): responsive dashboard and mobile drawer layout Co-authored-by: Cursor --- .../statistics-cards/StatisticsCardLine.vue | 26 + .../src/layouts/components/TheNavbar.vue | 33 ++ apps/admin-panel/src/layouts/main/Main.vue | 56 ++- apps/admin-panel/src/stores/layout.ts | 6 + apps/admin-panel/src/views/Dashboard.vue | 475 ++++++++++++------ 5 files changed, 451 insertions(+), 145 deletions(-) diff --git a/apps/admin-panel/src/components/statistics-cards/StatisticsCardLine.vue b/apps/admin-panel/src/components/statistics-cards/StatisticsCardLine.vue index 35423ac..67f772e 100644 --- a/apps/admin-panel/src/components/statistics-cards/StatisticsCardLine.vue +++ b/apps/admin-panel/src/components/statistics-cards/StatisticsCardLine.vue @@ -98,4 +98,30 @@ const iconComponent = computed(() => ICONS[props.icon ?? ''] ?? BarChartOutlined font-size: 12px; color: #888; } + +@media (max-width: 575px) { + .stat-card :deep(.ant-card-body) { + padding: 12px; + } + .stat-row { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } + .stat-icon { + width: 36px; + height: 36px; + font-size: 17px; + } + .stat-value { + font-size: 16px; + line-height: 1.4; + word-break: break-word; + } + .stat-title { + display: block; + font-size: 11px; + line-height: 1.5; + } +} diff --git a/apps/admin-panel/src/layouts/components/TheNavbar.vue b/apps/admin-panel/src/layouts/components/TheNavbar.vue index 1677cdc..9e115ad 100644 --- a/apps/admin-panel/src/layouts/components/TheNavbar.vue +++ b/apps/admin-panel/src/layouts/components/TheNavbar.vue @@ -126,9 +126,19 @@ async function onLogout() { align-items: center; gap: 12px; } +.nav-left { + min-width: 0; + flex: 1; +} +.nav-right { + flex-shrink: 0; +} .page-title { font-size: 16px; color: #1c2d56; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .breadcrumb { font-size: 13px; @@ -139,6 +149,29 @@ async function onLogout() { gap: 8px; cursor: pointer; } + +@media (max-width: 991px) { + .the-navbar { + height: 56px; + line-height: 56px; + padding: 0 8px 0 16px; + } + .nav-left { + gap: 4px; + } + .breadcrumb { + display: none; + } + .page-title { + font-size: 15px; + font-weight: 600; + } +} +@media (max-width: 575px) { + .user-name { + display: none; + } +} - + + + + + - +
@@ -41,6 +56,11 @@ const layout = useLayoutStore(); // collapsed/expanded across reloads instead of resetting every time. const collapsed = computed(() => layout.sidebarCollapsed); +function onToggleSidebar() { + if (layout.isSmallScreen) layout.setMobileNavOpen(!layout.mobileNavOpen); + else layout.setSidebarCollapsed(!collapsed.value); +} + // legacy meta.no_scroll -> the 'no-scroll' body class const isAppPage = computed(() => Boolean(route.meta?.no_scroll)); @@ -58,13 +78,23 @@ watch( () => route.path, () => { overriddenTitle.value = undefined; + layout.setMobileNavOpen(false); + }, +); + +watch( + () => layout.isSmallScreen, + (small) => { + if (!small) layout.setMobileNavOpen(false); }, ); + + + diff --git a/apps/admin-panel/src/stores/layout.ts b/apps/admin-panel/src/stores/layout.ts index d34255c..48722b3 100644 --- a/apps/admin-panel/src/stores/layout.ts +++ b/apps/admin-panel/src/stores/layout.ts @@ -27,10 +27,13 @@ export const useLayoutStore = defineStore('layout', { // legacy state.js: sidebar visibility for the vertical nav menu isSidebarActive: true, sidebarCollapsed: readPersistedCollapsed(), + // below the 992 breakpoint the sidebar is an off-canvas drawer; never persisted + mobileNavOpen: false, }), getters: { // legacy used breakpoints of 1200 (theme tour) and 992 (nav menu collapse) isSmallScreen: (s) => s.windowWidth < 992, + isMobile: (s) => s.windowWidth < 576, }, actions: { // legacy mutation: UPDATE_WINDOW_WIDTH @@ -41,6 +44,9 @@ export const useLayoutStore = defineStore('layout', { updateWindowScrollY(y: number) { this.scrollY = y; }, + setMobileNavOpen(value: boolean) { + this.mobileNavOpen = value; + }, toggleSidebar(value?: boolean) { this.isSidebarActive = value ?? !this.isSidebarActive; }, diff --git a/apps/admin-panel/src/views/Dashboard.vue b/apps/admin-panel/src/views/Dashboard.vue index 3b4fb0f..6b1b006 100644 --- a/apps/admin-panel/src/views/Dashboard.vue +++ b/apps/admin-panel/src/views/Dashboard.vue @@ -9,132 +9,162 @@ payloads unchanged plus the new chart aggregates used below. --> - -
- - - - -
- -
- - -
- - - - - - -
- - - - - - - -
- -
- - + +
+ - + + +
+ +
+ + +
+ + + + + + +
+ + + + + + + +
+ +
+ + + + + + + + + +
+ + +
+
+ تعداد سفارش (۳۰ روز) + {{ data.sumLastOrder ?? 0 }} +
+
+ مجموع وزن (۳۰ روز) + {{ data.sumLastOrderMonth ?? 0 }} گرم +
+
+ میانگین هر سفارش + {{ avgWeightPerOrder }} گرم +
+
+
+ + + - + > + + - - - + + +
- - - - - {{ data.sumLastOrder ?? 0 }} - - - {{ data.sumLastOrderMonth ?? 0 }} گرم - - - {{ avgWeightPerOrder }} گرم - - - - - - - - -
@@ -142,9 +172,14 @@ import { computed, onMounted, reactive, ref } from 'vue'; import StatisticsCardLine from '../components/statistics-cards/StatisticsCardLine.vue'; import { jdate, price } from '../utils/filters'; +import { useLayoutStore } from '../stores/layout'; import api from '../api/client'; const loading = ref(false); +const layout = useLayoutStore(); + +const chartHeight = computed(() => (layout.isMobile ? 240 : 300)); +const barChartHeight = computed(() => (layout.isMobile ? 240 : 280)); interface OrdersTrendPoint { date: string; @@ -231,43 +266,57 @@ const trendSeries = computed(() => { ]; }); const trendChartOptions = computed(() => ({ - chart: { toolbar: { show: false }, fontFamily: 'inherit' }, + chart: { toolbar: { show: false }, fontFamily: 'inherit', parentHeightOffset: 0 }, colors: [COLOR_PRIMARY, COLOR_SUCCESS], dataLabels: { enabled: false }, stroke: { curve: 'smooth', width: 2 }, fill: { type: 'gradient', gradient: { opacityFrom: 0.35, opacityTo: 0.05 } }, - legend: { position: 'top', horizontalAlign: 'right' }, + legend: { position: 'top', horizontalAlign: 'right', fontSize: layout.isMobile ? '12px' : '14px' }, + grid: { padding: layout.isMobile ? { left: 4, right: 4 } : {} }, xaxis: { - categories: (data.ordersTrend ?? []).map((d: OrdersTrendPoint) => jdate(d.date)), + // phones drop the year (1405/07/04 -> 07/04) so the edge labels aren't + // clipped; the tooltip below still shows the full date + categories: (data.ordersTrend ?? []).map((d: OrdersTrendPoint) => { + const label = String(jdate(d.date) ?? ''); + return layout.isMobile ? label.slice(5) : label; + }), // 30 daily labels overlap at rotate: 0 (see screenshot); angle them so // each date stays legible instead of running into its neighbours. + tickAmount: layout.isMobile ? 6 : undefined, labels: { rotate: -45, rotateAlways: true, hideOverlappingLabels: true, - style: { fontSize: '12px', fontWeight: 500 }, + style: { fontSize: layout.isMobile ? '10px' : '12px', fontWeight: 500 }, + }, + }, + yaxis: { labels: { style: { fontSize: layout.isMobile ? '10px' : '12px' } } }, + tooltip: { + shared: true, + x: { + formatter: (_v: unknown, opts: { dataPointIndex: number }) => + jdate((data.ordersTrend ?? [])[opts?.dataPointIndex]?.date), }, }, - tooltip: { shared: true }, })); // ── وضعیت سفارشات: donut ──────────────────────────────────────────────── const statusSeries = computed(() => (data.statusBreakdown ?? []).map((s: StatusBreakdownRow) => s.count)); const statusChartOptions = computed(() => ({ - chart: { fontFamily: 'inherit' }, + chart: { fontFamily: 'inherit', parentHeightOffset: 0 }, labels: (data.statusBreakdown ?? []).map((s: StatusBreakdownRow) => s.label), colors: [COLOR_WARNING, COLOR_PRIMARY, COLOR_PRIMARY, COLOR_WARNING, COLOR_WARNING, COLOR_SUCCESS, COLOR_SUCCESS, COLOR_DANGER], - legend: { position: 'bottom' }, + legend: { position: 'bottom', fontSize: layout.isMobile ? '11px' : '14px' }, dataLabels: { enabled: true }, })); // ── تفکیک عمده/تک: donut ──────────────────────────────────────────────── const channelSeries = computed(() => (data.channelBreakdown ?? []).map((c: ChannelBreakdownRow) => c.count)); const channelChartOptions = computed(() => ({ - chart: { fontFamily: 'inherit' }, + chart: { fontFamily: 'inherit', parentHeightOffset: 0 }, labels: (data.channelBreakdown ?? []).map((c: ChannelBreakdownRow) => c.label), colors: [COLOR_PRIMARY, COLOR_SUCCESS], - legend: { position: 'bottom' }, + legend: { position: 'bottom', fontSize: layout.isMobile ? '11px' : '14px' }, dataLabels: { enabled: true }, })); @@ -276,23 +325,35 @@ const agentChartSeries = computed(() => [ { name: 'گرم فروخته‌شده', data: (data.reagent ?? []).map((r: any) => Number(r.grams_sell ?? 0)) }, ]); const agentChartOptions = computed(() => ({ - chart: { toolbar: { show: false }, fontFamily: 'inherit' }, + chart: { toolbar: { show: false }, fontFamily: 'inherit', parentHeightOffset: 0 }, plotOptions: { bar: { horizontal: true, borderRadius: 4 } }, colors: [COLOR_PRIMARY], dataLabels: { enabled: true }, - xaxis: { categories: (data.reagent ?? []).map((r: any) => r.name) }, + xaxis: { + categories: (data.reagent ?? []).map((r: any) => r.name), + labels: { style: { fontSize: layout.isMobile ? '10px' : '12px' } }, + }, + yaxis: { labels: { maxWidth: layout.isMobile ? 120 : 160, style: { fontSize: layout.isMobile ? '11px' : '12px' } } }, })); -// ── محصولات پرفروش: bar ───────────────────────────────────────────────── +// ── محصولات پرفروش: bar (horizontal on phones, so long titles stay readable) const productChartSeries = computed(() => [ { name: 'وزن فروخته‌شده (گرم)', data: (data.topProducts ?? []).map((p: TopProductRow) => Number(p.weight ?? 0)) }, ]); const productChartOptions = computed(() => ({ - chart: { toolbar: { show: false }, fontFamily: 'inherit' }, - plotOptions: { bar: { borderRadius: 4, columnWidth: '55%' } }, + chart: { toolbar: { show: false }, fontFamily: 'inherit', parentHeightOffset: 0 }, + plotOptions: { + bar: layout.isMobile + ? { horizontal: true, borderRadius: 4, barHeight: '60%' } + : { borderRadius: 4, columnWidth: '55%' }, + }, colors: [COLOR_SUCCESS], dataLabels: { enabled: false }, - xaxis: { categories: (data.topProducts ?? []).map((p: TopProductRow) => p.title) }, + xaxis: { + categories: (data.topProducts ?? []).map((p: TopProductRow) => p.title), + labels: { style: { fontSize: layout.isMobile ? '10px' : '12px' } }, + }, + yaxis: { labels: { maxWidth: layout.isMobile ? 120 : 160, style: { fontSize: layout.isMobile ? '11px' : '12px' } } }, })); onMounted(async () => { @@ -316,11 +377,143 @@ onMounted(async () => {