<script lang="ts" setup>
|
import type { PageParam, PageResult } from '@vben/request';
|
|
import type { ErpPurchaseOrderApi } from '#/api/erp/purchase/order';
|
import type { ErpSaleOrderApi } from '#/api/erp/sale/order';
|
import type { MesHomeApi } from '#/api/mes/home';
|
|
import { computed, onMounted, ref } from 'vue';
|
import { useRouter } from 'vue-router';
|
|
import { Page } from '@vben/common-ui';
|
|
import { Col, Row } from 'ant-design-vue';
|
|
import { getPurchaseOrderPage } from '#/api/erp/purchase/order';
|
import { getSaleOrderPage } from '#/api/erp/sale/order';
|
import { getHomeSummary } from '#/api/mes/home';
|
|
import SummaryCard from '#/views/erp/home/modules/summary-card.vue';
|
|
import { defaultSummary } from './data';
|
import BizCards from './modules/biz-cards.vue';
|
import KpiCards from './modules/kpi-cards.vue';
|
import QuickActions from './modules/quick-actions.vue';
|
|
defineOptions({ name: 'DashboardAnalytics' });
|
|
const router = useRouter();
|
|
const greeting = computed(() => {
|
const hour = new Date().getHours();
|
if (hour < 6) return '夜深了';
|
if (hour < 9) return '早上好';
|
if (hour < 12) return '上午好';
|
if (hour < 14) return '中午好';
|
if (hour < 18) return '下午好';
|
return '晚上好';
|
});
|
|
const today = computed(() => {
|
const d = new Date();
|
const weekNames = ['日', '一', '二', '三', '四', '五', '六'];
|
return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日 星期${weekNames[d.getDay()]}`;
|
});
|
|
function handleNavigate(name: string) {
|
router.push({ name });
|
}
|
|
interface PriceSummary {
|
monthPrice: number;
|
todayPrice: number;
|
yearPrice: number;
|
yesterdayPrice: number;
|
}
|
|
const saleSummary = ref<PriceSummary>();
|
const purchaseSummary = ref<PriceSummary>();
|
const homeSummary = ref<MesHomeApi.Summary>(defaultSummary);
|
|
const PAGE_SIZE = 200;
|
|
/** 逐页拉取全部分页数据(后端 pageSize 上限 200) */
|
async function fetchAllPages<T>(
|
fetcher: (params: PageParam) => Promise<PageResult<T>>,
|
): Promise<T[]> {
|
const first = await fetcher({ pageNo: 1, pageSize: PAGE_SIZE });
|
const list: T[] = [...(first.list ?? [])];
|
const total = first.total ?? list.length;
|
const pageCount = Math.ceil(total / PAGE_SIZE);
|
for (let pageNo = 2; pageNo <= pageCount; pageNo++) {
|
const res = await fetcher({ pageNo, pageSize: PAGE_SIZE });
|
list.push(...(res.list ?? []));
|
}
|
return list;
|
}
|
|
function toDayStr(time?: Date | string): string {
|
if (!time) return '';
|
if (typeof time === 'string') return time.slice(0, 10);
|
const d = new Date(time);
|
if (Number.isNaN(d.getTime())) return '';
|
const m = String(d.getMonth() + 1).padStart(2, '0');
|
const day = String(d.getDate()).padStart(2, '0');
|
return `${d.getFullYear()}-${m}-${day}`;
|
}
|
|
/** 按订单时间聚合金额:今日 / 昨日 / 本月 / 今年 */
|
function aggregatePrice(
|
orders: Array<{ orderTime?: Date | string; totalPrice?: number }>,
|
): PriceSummary {
|
const now = new Date();
|
const today = toDayStr(now);
|
const yesterdayDate = new Date(now);
|
yesterdayDate.setDate(now.getDate() - 1);
|
const yesterday = toDayStr(yesterdayDate);
|
const month = today.slice(0, 7);
|
const year = today.slice(0, 4);
|
|
const summary: PriceSummary = {
|
monthPrice: 0,
|
todayPrice: 0,
|
yearPrice: 0,
|
yesterdayPrice: 0,
|
};
|
for (const order of orders) {
|
const day = toDayStr(order.orderTime);
|
if (!day) continue;
|
const price = order.totalPrice ?? 0;
|
if (day === today) summary.todayPrice += price;
|
if (day === yesterday) summary.yesterdayPrice += price;
|
if (day.startsWith(month)) summary.monthPrice += price;
|
if (day.startsWith(year)) summary.yearPrice += price;
|
}
|
return summary;
|
}
|
|
async function loadStatistics() {
|
try {
|
const saleOrders = await fetchAllPages<ErpSaleOrderApi.SaleOrder>(
|
getSaleOrderPage,
|
);
|
saleSummary.value = aggregatePrice(saleOrders);
|
} catch {
|
/* 接口异常或无权限 */
|
}
|
try {
|
const purchaseOrders = await fetchAllPages<ErpPurchaseOrderApi.PurchaseOrder>(
|
getPurchaseOrderPage,
|
);
|
purchaseSummary.value = aggregatePrice(purchaseOrders);
|
} catch {
|
/* 接口异常或无权限 */
|
}
|
}
|
|
async function loadHomeSummary() {
|
try {
|
homeSummary.value = await getHomeSummary();
|
} catch {
|
/* 接口异常或无权限 */
|
}
|
}
|
|
onMounted(() => {
|
loadHomeSummary();
|
loadStatistics();
|
});
|
</script>
|
|
<template>
|
<Page auto-content-height>
|
<!-- 欢迎栏 -->
|
<div class="glass-card mb-6 flex items-center justify-between rounded-2xl p-6">
|
<div>
|
<h1 class="bg-gradient-to-r from-blue-600 via-purple-600 to-pink-500 bg-clip-text text-2xl font-bold text-transparent dark:from-blue-400 dark:via-purple-400 dark:to-pink-400">
|
{{ greeting }},欢迎回来
|
</h1>
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ today }}</p>
|
</div>
|
<div class="hidden items-center gap-2 sm:flex">
|
<div class="flex items-center gap-1.5 rounded-full bg-emerald-50 px-3 py-1.5 text-xs font-medium text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-400">
|
<span class="relative flex size-2">
|
<span class="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-75" />
|
<span class="relative inline-flex size-2 rounded-full bg-emerald-500" />
|
</span>
|
系统运行中
|
</div>
|
</div>
|
</div>
|
|
<!-- 质量 / 设备 KPI -->
|
<KpiCards :summary="homeSummary" @navigate="handleNavigate" />
|
|
<!-- ERP/WMS/BPM 经营 KPI -->
|
<BizCards @navigate="handleNavigate" />
|
|
<!-- 销售 / 采购 金额概览 -->
|
<div class="mb-4">
|
<SummaryCard
|
:purchase-summary="purchaseSummary"
|
:sale-summary="saleSummary"
|
/>
|
</div>
|
|
<!-- 快捷操作 -->
|
<Row :gutter="16" class="mb-4">
|
<Col :span="24">
|
<QuickActions @navigate="handleNavigate" />
|
</Col>
|
</Row>
|
</Page>
|
</template>
|
|
<style scoped>
|
.glass-card {
|
background: linear-gradient(135deg, rgba(255,255,255,0.8), rgba(255,255,255,0.5));
|
backdrop-filter: blur(20px);
|
-webkit-backdrop-filter: blur(20px);
|
border: 1px solid rgba(255,255,255,0.6);
|
box-shadow:
|
0 8px 32px rgba(0,0,0,0.04),
|
0 2px 8px rgba(0,0,0,0.02),
|
inset 0 1px 0 rgba(255,255,255,0.8);
|
}
|
|
.dark .glass-card {
|
background: linear-gradient(135deg, rgba(30,30,50,0.7), rgba(20,20,40,0.5));
|
border: 1px solid rgba(255,255,255,0.08);
|
box-shadow:
|
0 8px 32px rgba(0,0,0,0.3),
|
0 2px 8px rgba(0,0,0,0.2),
|
inset 0 1px 0 rgba(255,255,255,0.05);
|
}
|
</style>
|