Files
gao-customer-h5/src/pages/product/OrderDataCharts.vue
T
2026-09-06 23:20:26 +08:00

338 lines
8.3 KiB
Vue

<template>
<div v-can="permissionCodes.stat.read" class="gao-chart-page order-data-charts">
<section class="hero">
<div class="eyebrow" v-text="'订单经营分析'" />
<h1 v-text="'客户经营看板'" />
<p v-text="`${currentStoreStore.currentStoreName} · 订单、收款与客户贡献 · 近 1 个月`" />
</section>
<div v-if="!storeId" class="no-store">
<t-empty description="暂无可统计的门店" />
</div>
<template v-else>
<section class="summary">
<div v-for="item in summaryList" :key="item.label" class="summary-item">
<span v-text="item.label" />
<strong v-text="item.value" />
</div>
</section>
<div class="chart-list">
<order-daily-chart
v-entrance-transition
:date-text-list="dateTextList"
:list="dailyList"
:loading="isLoading"
/>
<div class="two-columns">
<order-type-chart
v-entrance-transition="70"
:list="typeList"
:loading="isLoading"
/>
<order-payment-chart
v-entrance-transition="140"
:list="paymentList"
:loading="isLoading"
/>
</div>
<div class="two-columns">
<order-status-chart
v-entrance-transition="210"
:list="statusList"
:loading="isLoading"
/>
<customer-order-rank-chart
v-entrance-transition="280"
:list="customerRankList"
:loading="isLoading"
/>
</div>
<order-receivable-card
v-entrance-transition="350"
:receivable="receivable"
/>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import GaoSalesOrderChartAPI from "@/api/GaoSalesOrderChartAPI";
import CustomerOrderRankChart from "@/pages/product/component/CustomerOrderRankChart.vue";
import OrderDailyChart from "@/pages/product/component/OrderDailyChart.vue";
import OrderPaymentChart from "@/pages/product/component/OrderPaymentChart.vue";
import OrderReceivableCard from "@/pages/product/component/OrderReceivableCard.vue";
import OrderStatusChart from "@/pages/product/component/OrderStatusChart.vue";
import OrderTypeChart from "@/pages/product/component/OrderTypeChart.vue";
import type {
GaoCustomerOrderRankView,
GaoSalesOrderDailyStatView,
GaoSalesOrderPaymentStatView,
GaoSalesOrderReceivableStatView,
GaoSalesOrderStatusStatView,
GaoSalesOrderTypeStatView
} from "@/types/GaoChart";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import { useNavBarStore } from "@/store/navBarStore";
import { getCurrentStoreId } from "@/utils/store";
import { clearStoreChartCache } from "@/utils/storeChartCache";
import { getErrorMessage } from "@/utils/error";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
import { buildDateTextList, formatMoney, formatQuantity, toUnixRange } from "@/pages/product/component/chartUtils";
interface SummaryItem {
label: string;
value: string;
}
defineOptions({
name: "OrderDataCharts"
});
const currentStoreStore = useCurrentStoreStore();
const navBarStore = useNavBarStore();
const dateRange = buildDefaultDateRange();
const dateTextList = buildDateTextList(dateRange[0], dateRange[1]);
const dailyList = ref<GaoSalesOrderDailyStatView[]>([]);
const typeList = ref<GaoSalesOrderTypeStatView[]>([]);
const paymentList = ref<GaoSalesOrderPaymentStatView[]>([]);
const statusList = ref<GaoSalesOrderStatusStatView[]>([]);
const customerRankList = ref<GaoCustomerOrderRankView[]>([]);
const receivable = ref<GaoSalesOrderReceivableStatView>({});
const isLoading = ref(false);
let requestId = 0;
const storeId = computed(() => currentStoreStore.currentStoreId || getCurrentStoreId());
const summaryList = computed<SummaryItem[]>(() => [
{
label: "订单数",
value: `${formatQuantity(receivable.value.orderCount)} 单`
},
{
label: "成交金额",
value: formatMoney(receivable.value.saleAmount)
},
{
label: "有效收款",
value: formatMoney(receivable.value.paidAmount)
},
{
label: "未收金额",
value: formatMoney(receivable.value.unpaidAmount)
},
{
label: "净收款",
value: formatMoney(receivable.value.netPaidAmount)
}
]);
watch(storeId, async () => {
await loadCharts();
}, { immediate: true });
onMounted(async () => {
navBarStore.setRightIcon("refresh", reload);
if (!currentStoreStore.currentStoreId) {
await currentStoreStore.loadLoginStore();
}
});
onUnmounted(() => {
navBarStore.clearRight();
});
async function reload(): Promise<void> {
clearStoreChartCache();
await loadCharts();
}
async function loadCharts(): Promise<void> {
if (!storeId.value) {
clearData();
return;
}
const currentRequestId = ++requestId;
isLoading.value = true;
try {
const range = toUnixRange(dateRange);
const request = { storeId: storeId.value, ...range, topN: 10 };
const [daily, type, payment, status, receivableData, customerRank] = await Promise.all([
GaoSalesOrderChartAPI.daily(request),
GaoSalesOrderChartAPI.type(request),
GaoSalesOrderChartAPI.payment(request),
GaoSalesOrderChartAPI.status(request),
GaoSalesOrderChartAPI.receivable(request),
GaoSalesOrderChartAPI.customerRank(request)
]);
if (currentRequestId !== requestId) {
return;
}
dailyList.value = daily || [];
typeList.value = type || [];
paymentList.value = payment || [];
statusList.value = status || [];
receivable.value = receivableData || {};
customerRankList.value = customerRank || [];
} catch (error) {
if (currentRequestId === requestId) {
Message.error(getErrorMessage(error, "订单图表加载失败"));
}
} finally {
if (currentRequestId === requestId) {
isLoading.value = false;
}
}
}
function clearData(): void {
isLoading.value = false;
dailyList.value = [];
typeList.value = [];
paymentList.value = [];
statusList.value = [];
receivable.value = {};
customerRankList.value = [];
}
function buildDefaultDateRange(): [string, string] {
const end = getDateEnd(new Date());
const today = getDateStart(end);
const beginMonth = new Date(today.getFullYear(), today.getMonth() - 1, 1, 0, 0, 0, 0);
const lastDayOfBeginMonth = new Date(beginMonth.getFullYear(), beginMonth.getMonth() + 1, 0).getDate();
const begin = new Date(beginMonth);
begin.setDate(Math.min(today.getDate(), lastDayOfBeginMonth));
return [formatDateTime(begin), formatDateTime(end)];
}
function getDateStart(date: Date): Date {
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0);
}
function getDateEnd(date: Date): Date {
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999);
}
function formatDateTime(date: Date): string {
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
}
function pad(value: number): string {
return String(value).padStart(2, "0");
}
</script>
<style scoped lang="less">
.gao-chart-page {
gap: 1rem;
padding: calc(var(--app-nav-height) + 1rem) 1rem 1rem;
display: flex;
flex-direction: column;
.hero {
padding: .25rem .25rem 0;
.eyebrow {
color: var(--td-brand-color);
font-size: .6875rem;
font-weight: 700;
letter-spacing: .12em;
}
h1 {
color: #0f172a;
margin: .25rem 0 0;
font-size: 1.5rem;
line-height: 1.3;
}
p {
color: #7b889d;
margin: .375rem 0 0;
font-size: .8125rem;
}
}
.summary {
gap: .625rem;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.summary-item {
padding: .75rem .625rem;
background: rgba(255, 255, 255, .92);
box-shadow: var(--td-shadow-1);
border-radius: 1rem;
span {
color: #52607a;
display: block;
font-size: .75rem;
}
strong {
color: #0f172a;
margin-top: .25rem;
display: block;
font-size: 1rem;
font-weight: 700;
}
}
.chart-list {
gap: 1rem;
display: flex;
flex-direction: column;
}
.two-columns {
gap: 1rem;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.no-store {
padding: 1.5rem 0;
display: flex;
align-items: center;
justify-content: center;
}
}
:global(html.theme-dark) {
.gao-chart-page {
.hero {
h1 {
color: #e2e8f0;
}
p {
color: #94a3b8;
}
}
.summary-item {
background: rgba(15, 23, 42, .88);
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
span {
color: #94a3b8;
}
strong {
color: #e2e8f0;
}
}
}
}
@media (max-width: 42rem) {
.gao-chart-page {
.two-columns {
grid-template-columns: 1fr;
}
}
}
</style>