ui(admin-panel): responsive dashboard and mobile drawer layout
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!--
|
||||
|
||||
@@ -12,10 +12,25 @@
|
||||
sidebar/navbar that used to scroll away with it.
|
||||
-->
|
||||
<a-layout class="layout--main" :class="{ 'no-scroll': isAppPage }">
|
||||
<VerticalNavMenu :collapsed="collapsed" />
|
||||
<VerticalNavMenu v-if="!layout.isSmallScreen" :collapsed="collapsed" />
|
||||
|
||||
<a-drawer
|
||||
v-else
|
||||
:open="layout.mobileNavOpen"
|
||||
placement="right"
|
||||
:width="260"
|
||||
:closable="false"
|
||||
root-class-name="mobile-nav-drawer"
|
||||
@close="layout.setMobileNavOpen(false)"
|
||||
>
|
||||
<VerticalNavMenu />
|
||||
</a-drawer>
|
||||
|
||||
<a-layout>
|
||||
<TheNavbar :collapsed="collapsed" @toggle-sidebar="layout.setSidebarCollapsed(!collapsed)" />
|
||||
<TheNavbar
|
||||
:collapsed="layout.isSmallScreen ? !layout.mobileNavOpen : collapsed"
|
||||
@toggle-sidebar="onToggleSidebar"
|
||||
/>
|
||||
|
||||
<a-layout-content id="content-area">
|
||||
<div class="content-wrapper">
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout--main {
|
||||
height: 100vh;
|
||||
/* --vh is set from window.innerHeight in App.vue, so mobile browser chrome
|
||||
(address bar) is excluded from the height */
|
||||
height: calc(var(--vh, 1vh) * 100);
|
||||
overflow: hidden; /* the sidebar and navbar are pinned; only #content-area scrolls */
|
||||
}
|
||||
.layout--main > :deep(.ant-layout) {
|
||||
@@ -78,8 +108,14 @@ watch(
|
||||
gap underneath on short lists. This is also the ONLY element that
|
||||
scrolls — the navbar above it and the sidebar beside it stay fixed. */
|
||||
display: flex;
|
||||
height: calc(100vh - 61px); /* 61px matches TheNavbar's fixed height */
|
||||
height: calc(var(--vh, 1vh) * 100 - 61px); /* 61px matches TheNavbar's fixed height */
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
@media (max-width: 991px) {
|
||||
#content-area {
|
||||
height: calc(var(--vh, 1vh) * 100 - 56px); /* TheNavbar is 56px below 992 */
|
||||
}
|
||||
}
|
||||
.content-wrapper,
|
||||
.content-area__content {
|
||||
@@ -87,5 +123,17 @@ watch(
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Unscoped: <a-drawer> teleports to <body>; contained by root-class-name. -->
|
||||
<style>
|
||||
.mobile-nav-drawer .ant-drawer-body {
|
||||
padding: 0;
|
||||
}
|
||||
.mobile-nav-drawer .vertical-nav-menu {
|
||||
height: 100%;
|
||||
border-left: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -9,132 +9,162 @@
|
||||
payloads unchanged plus the new chart aggregates used below.
|
||||
-->
|
||||
<a-spin :spinning="loading">
|
||||
<!-- ── KPI row: every stat the two legacy dashboards showed, preserved ── -->
|
||||
<div class="grid-4">
|
||||
<StatisticsCardLine
|
||||
icon="UsersIcon"
|
||||
color="warning"
|
||||
:statistic="`${data.customerCount ?? 0} نفر`"
|
||||
statistic-title="مشتریان جدید (۳۰ روز گذشته)"
|
||||
/>
|
||||
<StatisticsCardLine
|
||||
icon="ShoppingBagIcon"
|
||||
color="primary"
|
||||
:statistic="`${data.allgram ?? 0} گرم`"
|
||||
statistic-title="طلای فروش رفته (۳۰ روز گذشته)"
|
||||
/>
|
||||
<StatisticsCardLine
|
||||
icon="ShoppingBagIcon"
|
||||
color="success"
|
||||
:statistic="`${data.ordercount ?? 0} سفارش`"
|
||||
statistic-title="تعداد سفارشات (۳۰ روز گذشته)"
|
||||
/>
|
||||
<StatisticsCardLine
|
||||
icon="DollarSignIcon"
|
||||
color="danger"
|
||||
:statistic="goldPriceLabel"
|
||||
statistic-title="نرخ هر گرم طلای ۱۸ عیار"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid-2 mt-4">
|
||||
<StatisticsCardLine
|
||||
icon="BarChartIcon"
|
||||
color="primary"
|
||||
:statistic="`${data.sumLastOrder ?? 0} سفارش`"
|
||||
statistic-title="تعداد سفارشات تحویلشده (۳۰ روز گذشته)"
|
||||
/>
|
||||
<StatisticsCardLine
|
||||
icon="CreditCardIcon"
|
||||
color="warning"
|
||||
:statistic="`${totalDebtLabel} گرم`"
|
||||
statistic-title="مجموع بدهی طلای مشتریان"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- ── روند سفارشها: 30-day line chart, تک vs عمده ── -->
|
||||
<a-card title="روند سفارشها (۳۰ روز گذشته)" class="mt-4">
|
||||
<apexchart
|
||||
type="area"
|
||||
height="300"
|
||||
:options="trendChartOptions"
|
||||
:series="trendSeries"
|
||||
/>
|
||||
</a-card>
|
||||
|
||||
<div class="grid-2 mt-4">
|
||||
<a-card title="وضعیت سفارشات (۳۰ روز گذشته)">
|
||||
<apexchart type="donut" height="300" :options="statusChartOptions" :series="statusSeries" />
|
||||
</a-card>
|
||||
|
||||
<a-card title="تفکیک عمده / تک (۳۰ روز گذشته)">
|
||||
<apexchart type="donut" height="300" :options="channelChartOptions" :series="channelSeries" />
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<div class="grid-2 mt-4">
|
||||
<a-card title="عملکرد کارشناسان فروش">
|
||||
<apexchart
|
||||
v-if="agentChartSeries[0]?.data.length"
|
||||
type="bar"
|
||||
height="280"
|
||||
:options="agentChartOptions"
|
||||
:series="agentChartSeries"
|
||||
<div class="dashboard">
|
||||
<!-- ── KPI row: every stat the two legacy dashboards showed, preserved ── -->
|
||||
<div class="kpi-grid kpi-grid--4">
|
||||
<StatisticsCardLine
|
||||
icon="UsersIcon"
|
||||
color="warning"
|
||||
:statistic="`${data.customerCount ?? 0} نفر`"
|
||||
statistic-title="مشتریان جدید (۳۰ روز گذشته)"
|
||||
/>
|
||||
<a-table
|
||||
class="mt-3"
|
||||
size="small"
|
||||
:data-source="data.reagent"
|
||||
:columns="agentColumns"
|
||||
:pagination="false"
|
||||
<StatisticsCardLine
|
||||
icon="ShoppingBagIcon"
|
||||
color="primary"
|
||||
:statistic="`${data.allgram ?? 0} گرم`"
|
||||
statistic-title="طلای فروش رفته (۳۰ روز گذشته)"
|
||||
/>
|
||||
<StatisticsCardLine
|
||||
icon="ShoppingBagIcon"
|
||||
color="success"
|
||||
:statistic="`${data.ordercount ?? 0} سفارش`"
|
||||
statistic-title="تعداد سفارشات (۳۰ روز گذشته)"
|
||||
/>
|
||||
<StatisticsCardLine
|
||||
icon="DollarSignIcon"
|
||||
color="danger"
|
||||
:statistic="goldPriceLabel"
|
||||
statistic-title="نرخ هر گرم طلای ۱۸ عیار"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="kpi-grid kpi-grid--2">
|
||||
<StatisticsCardLine
|
||||
icon="BarChartIcon"
|
||||
color="primary"
|
||||
:statistic="`${data.sumLastOrder ?? 0} سفارش`"
|
||||
statistic-title="تعداد سفارشات تحویلشده (۳۰ روز گذشته)"
|
||||
/>
|
||||
<StatisticsCardLine
|
||||
icon="CreditCardIcon"
|
||||
color="warning"
|
||||
:statistic="`${totalDebtLabel} گرم`"
|
||||
statistic-title="مجموع بدهی طلای مشتریان"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- ── روند سفارشها: 30-day line chart, تک vs عمده ── -->
|
||||
<a-card title="روند سفارشها (۳۰ روز گذشته)" class="dash-card">
|
||||
<apexchart
|
||||
type="area"
|
||||
:height="chartHeight"
|
||||
:options="trendChartOptions"
|
||||
:series="trendSeries"
|
||||
/>
|
||||
</a-card>
|
||||
|
||||
<div class="card-grid">
|
||||
<a-card title="وضعیت سفارشات (۳۰ روز گذشته)" class="dash-card">
|
||||
<apexchart type="donut" :height="chartHeight" :options="statusChartOptions" :series="statusSeries" />
|
||||
</a-card>
|
||||
|
||||
<a-card title="تفکیک عمده / تک (۳۰ روز گذشته)" class="dash-card">
|
||||
<apexchart type="donut" :height="chartHeight" :options="channelChartOptions" :series="channelSeries" />
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<div class="card-grid">
|
||||
<a-card title="عملکرد کارشناسان فروش" class="dash-card">
|
||||
<apexchart
|
||||
v-if="agentChartSeries[0]?.data.length"
|
||||
type="bar"
|
||||
:height="barChartHeight"
|
||||
:options="agentChartOptions"
|
||||
:series="agentChartSeries"
|
||||
/>
|
||||
<a-table
|
||||
class="dash-table"
|
||||
size="small"
|
||||
:data-source="data.reagent"
|
||||
:columns="agentColumns"
|
||||
:pagination="false"
|
||||
:scroll="{ x: 'max-content' }"
|
||||
row-key="id"
|
||||
/>
|
||||
</a-card>
|
||||
|
||||
<a-card title="محصولات پرفروش (۳۰ روز گذشته)" class="dash-card">
|
||||
<apexchart
|
||||
v-if="productChartSeries[0]?.data.length"
|
||||
type="bar"
|
||||
:height="barChartHeight"
|
||||
:options="productChartOptions"
|
||||
:series="productChartSeries"
|
||||
/>
|
||||
<a-empty v-else description="دادهای برای نمایش وجود ندارد" />
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<a-card title="میزان فروش" class="dash-card">
|
||||
<div class="sales-summary">
|
||||
<div class="sales-summary__item">
|
||||
<span class="sales-summary__label">تعداد سفارش (۳۰ روز)</span>
|
||||
<span class="sales-summary__value">{{ data.sumLastOrder ?? 0 }}</span>
|
||||
</div>
|
||||
<div class="sales-summary__item">
|
||||
<span class="sales-summary__label">مجموع وزن (۳۰ روز)</span>
|
||||
<span class="sales-summary__value">{{ data.sumLastOrderMonth ?? 0 }} گرم</span>
|
||||
</div>
|
||||
<div class="sales-summary__item">
|
||||
<span class="sales-summary__label">میانگین هر سفارش</span>
|
||||
<span class="sales-summary__value">{{ avgWeightPerOrder }} گرم</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-card title="سفارشات هفته جاری" class="dash-card">
|
||||
<a-list
|
||||
v-if="layout.isMobile"
|
||||
class="order-list"
|
||||
:data-source="data.lastOrder"
|
||||
:pagination="data.lastOrder?.length > 10 ? { pageSize: 10, size: 'small', align: 'center' } : false"
|
||||
row-key="id"
|
||||
/>
|
||||
</a-card>
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item class="order-item">
|
||||
<div class="order-item__head">
|
||||
<span class="order-item__name">{{ item.name }}</span>
|
||||
<a-tag :color="tagColor(item.statusColor)" class="order-item__tag">{{ item.sts_str }}</a-tag>
|
||||
</div>
|
||||
<div class="order-item__customer">{{ item.customer }}</div>
|
||||
<div class="order-item__meta">
|
||||
<span>{{ item.w }} گرم</span>
|
||||
<span>{{ jdate(item.caj) }}</span>
|
||||
</div>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
|
||||
<a-card title="محصولات پرفروش (۳۰ روز گذشته)">
|
||||
<apexchart
|
||||
v-if="productChartSeries[0]?.data.length"
|
||||
type="bar"
|
||||
height="280"
|
||||
:options="productChartOptions"
|
||||
:series="productChartSeries"
|
||||
/>
|
||||
<a-empty v-else description="دادهای برای نمایش وجود ندارد" />
|
||||
<a-table
|
||||
v-else
|
||||
size="small"
|
||||
:data-source="data.lastOrder"
|
||||
:columns="orderColumns"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
:scroll="{ x: 'max-content' }"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'sts_str'">
|
||||
<a-tag :color="tagColor(record.statusColor)">{{ record.sts_str }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'caj'">
|
||||
{{ jdate(record.caj) }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<a-card title="میزان فروش" class="mt-4">
|
||||
<a-descriptions bordered size="small" :column="3">
|
||||
<a-descriptions-item label="تعداد سفارش (۳۰ روز)">
|
||||
{{ data.sumLastOrder ?? 0 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="مجموع وزن (۳۰ روز)">
|
||||
{{ data.sumLastOrderMonth ?? 0 }} گرم
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="میانگین هر سفارش">
|
||||
{{ avgWeightPerOrder }} گرم
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
|
||||
<a-card title="سفارشات هفته جاری" class="mt-4">
|
||||
<a-table
|
||||
size="small"
|
||||
:data-source="data.lastOrder"
|
||||
:columns="orderColumns"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'sts_str'">
|
||||
<a-tag :color="tagColor(record.statusColor)">{{ record.sts_str }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'caj'">
|
||||
{{ jdate(record.caj) }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
@@ -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 () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* .grid-2/.grid-3/.grid-4 come from the global utilities.less (dedup) */
|
||||
.mt-4 {
|
||||
margin-top: 1rem;
|
||||
.dashboard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
.mt-3 {
|
||||
margin-top: 0.75rem;
|
||||
|
||||
.kpi-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
.kpi-grid--4 {
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
}
|
||||
.kpi-grid--2,
|
||||
.card-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
}
|
||||
.card-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
.card-grid > *,
|
||||
.kpi-grid > * {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dash-table {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* ── میزان فروش ── */
|
||||
.sales-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.sales-summary__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
.sales-summary__item + .sales-summary__item {
|
||||
border-right: 1px solid #f0f0f0;
|
||||
}
|
||||
.sales-summary__label {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
.sales-summary__value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1c2d56;
|
||||
}
|
||||
|
||||
/* ── سفارشات هفته جاری (phone list) ── */
|
||||
.order-list :deep(.ant-list-pagination) {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.order-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 4px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.order-item__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.order-item__name {
|
||||
font-weight: 600;
|
||||
color: #1c2d56;
|
||||
}
|
||||
.order-item__tag {
|
||||
margin: 0;
|
||||
}
|
||||
.order-item__customer {
|
||||
font-size: 13px;
|
||||
color: #444;
|
||||
}
|
||||
.order-item__meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.dashboard,
|
||||
.kpi-grid,
|
||||
.card-grid {
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.kpi-grid--4,
|
||||
.kpi-grid--2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.card-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.dash-card :deep(.ant-card-head) {
|
||||
min-height: 44px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.dash-card :deep(.ant-card-head-title) {
|
||||
font-size: 14px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.dash-card :deep(.ant-card-body) {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.sales-summary {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
.sales-summary__item {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
.sales-summary__item + .sales-summary__item {
|
||||
border-right: none;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
.sales-summary__value {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user