diff --git a/src/api/GaoProductChartAPI.ts b/src/api/GaoProductChartAPI.ts new file mode 100644 index 0000000..a0dc866 --- /dev/null +++ b/src/api/GaoProductChartAPI.ts @@ -0,0 +1,74 @@ +import { axios } from "timi-web-api"; +import type { + GaoProductCategoryStatView, + GaoProductChartReq, + GaoProductFulfillmentDailyStatView, + GaoProductFulfillmentRankView, + GaoProductSalesDailyStatView, + GaoProductSalesRankView, + GaoProductStockHealthView, + GaoProductStockMovementDailyStatView, + GaoProductTypeStatView, + GaoProductSkuUnitSalesRankView +} from "@/types/GaoChart"; +import { getStoreChartCache } from "@/utils/storeChartCache"; +import { withCurrentStoreId } from "@/utils/store"; + +const BASE_URI = "/gao/product/chart"; + +async function salesDaily(req: GaoProductChartReq): Promise { + return await requestList("sales-daily", "/sales/daily", req); +} + +async function salesRank(req: GaoProductChartReq): Promise { + return await requestList("sales-rank", "/sales/rank", req); +} + +async function unitSalesRank(req: GaoProductChartReq): Promise { + const list = await requestList>("sales-unit-rank", "/sales/unit/rank", req); + return list.map(item => ({ + ...item, + skuUnitId: item.skuUnitId || (typeof item["productUnitId"] === "string" ? item["productUnitId"] : undefined) + })); +} + +async function fulfillmentDaily(req: GaoProductChartReq): Promise { + return await requestList("fulfillment-daily", "/fulfillment/daily", req); +} + +async function fulfillmentRank(req: GaoProductChartReq): Promise { + return await requestList("fulfillment-rank", "/fulfillment/rank", req); +} + +async function stockMovementDaily(req: GaoProductChartReq): Promise { + return await requestList("stock-movement-daily", "/stock/movement/daily", req); +} + +async function stockHealth(req: GaoProductChartReq): Promise { + return await requestList("stock-health", "/stock/health", req); +} + +async function type(req: GaoProductChartReq): Promise { + return await requestList("structure-type", "/structure/type", req); +} + +async function category(req: GaoProductChartReq): Promise { + return await requestList("structure-category", "/structure/category", req); +} + +async function requestList(name: string, path: string, req: GaoProductChartReq): Promise { + const request = withCurrentStoreId(req); + return await getStoreChartCache(`gao-product:${name}:${JSON.stringify(request)}`, async () => await axios.post(`${BASE_URI}${path}`, request)); +} + +export default { + salesDaily, + salesRank, + unitSalesRank, + fulfillmentDaily, + fulfillmentRank, + stockMovementDaily, + stockHealth, + type, + category +}; diff --git a/src/api/GaoSalesOrderChartAPI.ts b/src/api/GaoSalesOrderChartAPI.ts new file mode 100644 index 0000000..24100ab --- /dev/null +++ b/src/api/GaoSalesOrderChartAPI.ts @@ -0,0 +1,83 @@ +import { axios } from "timi-web-api"; +import type { + GaoCustomerOrderProductRankView, + GaoCustomerOrderRankView, + GaoCustomerRepeatDailyStatView, + GaoCustomerRepeatIntervalStatView, + GaoSalesOrderCalendarStatView, + GaoSalesOrderChartReq, + GaoSalesOrderConsumptionDailyStatView, + GaoSalesOrderDailyStatView, + GaoSalesOrderPaymentStatView, + GaoSalesOrderReceivableStatView, + GaoSalesOrderStatusStatView, + GaoSalesOrderTypeStatView +} from "@/types/GaoChart"; +import { getStoreChartCache } from "@/utils/storeChartCache"; +import { withCurrentStoreId } from "@/utils/store"; + +const BASE_URI = "/gao/sales/order/chart"; + +async function daily(req: GaoSalesOrderChartReq): Promise { + return await requestList("daily", "/daily", req); +} + +async function type(req: GaoSalesOrderChartReq): Promise { + return await requestList("type", "/type", req); +} + +async function payment(req: GaoSalesOrderChartReq): Promise { + return await requestList("payment", "/payment", req); +} + +async function status(req: GaoSalesOrderChartReq): Promise { + return await requestList("status", "/status", req); +} + +async function receivable(req: GaoSalesOrderChartReq): Promise { + const request = withCurrentStoreId(req); + return await getStoreChartCache(`gao-order:receivable:${JSON.stringify(request)}`, async () => await axios.post(`${BASE_URI}/receivable`, request)); +} + +async function customerRank(req: GaoSalesOrderChartReq): Promise { + return await requestList("customer-rank", "/customer/rank", req); +} + +async function customerProductRank(req: GaoSalesOrderChartReq): Promise { + return await requestList("customer-product-rank", "/customer/product/rank", req); +} + +async function consumptionDaily(req: GaoSalesOrderChartReq): Promise { + return await requestList("customer-consumption-daily", "/customer/consumption/daily", req); +} + +async function calendar(req: GaoSalesOrderChartReq): Promise { + return await requestList("customer-calendar", "/customer/calendar", req); +} + +async function repeatDaily(req: GaoSalesOrderChartReq): Promise { + return await requestList("customer-repeat-daily", "/customer/repeat/daily", req); +} + +async function repeatInterval(req: GaoSalesOrderChartReq): Promise { + return await requestList("customer-repeat-interval", "/customer/repeat/interval", req); +} + +async function requestList(name: string, path: string, req: GaoSalesOrderChartReq): Promise { + const request = withCurrentStoreId(req); + return await getStoreChartCache(`gao-order:${name}:${JSON.stringify(request)}`, async () => await axios.post(`${BASE_URI}${path}`, request)); +} + +export default { + daily, + type, + payment, + status, + receivable, + customerRank, + customerProductRank, + consumptionDaily, + calendar, + repeatDaily, + repeatInterval +}; diff --git a/src/pages/product/OrderDataCharts.vue b/src/pages/product/OrderDataCharts.vue new file mode 100644 index 0000000..9b987dd --- /dev/null +++ b/src/pages/product/OrderDataCharts.vue @@ -0,0 +1,337 @@ + + + + + diff --git a/src/pages/product/ProductDataCharts.vue b/src/pages/product/ProductDataCharts.vue new file mode 100644 index 0000000..36980fa --- /dev/null +++ b/src/pages/product/ProductDataCharts.vue @@ -0,0 +1,339 @@ + + + + + diff --git a/src/pages/product/component/CustomerOrderRankChart.vue b/src/pages/product/component/CustomerOrderRankChart.vue new file mode 100644 index 0000000..a9c6ab9 --- /dev/null +++ b/src/pages/product/component/CustomerOrderRankChart.vue @@ -0,0 +1,51 @@ + + + diff --git a/src/pages/product/component/GaoChartCard.vue b/src/pages/product/component/GaoChartCard.vue new file mode 100644 index 0000000..d7f9b6e --- /dev/null +++ b/src/pages/product/component/GaoChartCard.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/src/pages/product/component/OrderDailyChart.vue b/src/pages/product/component/OrderDailyChart.vue new file mode 100644 index 0000000..79bb46e --- /dev/null +++ b/src/pages/product/component/OrderDailyChart.vue @@ -0,0 +1,81 @@ + + + diff --git a/src/pages/product/component/OrderPaymentChart.vue b/src/pages/product/component/OrderPaymentChart.vue new file mode 100644 index 0000000..67f8b74 --- /dev/null +++ b/src/pages/product/component/OrderPaymentChart.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/pages/product/component/OrderReceivableCard.vue b/src/pages/product/component/OrderReceivableCard.vue new file mode 100644 index 0000000..d73f200 --- /dev/null +++ b/src/pages/product/component/OrderReceivableCard.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/src/pages/product/component/OrderStatusChart.vue b/src/pages/product/component/OrderStatusChart.vue new file mode 100644 index 0000000..9829fbc --- /dev/null +++ b/src/pages/product/component/OrderStatusChart.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/pages/product/component/OrderTypeChart.vue b/src/pages/product/component/OrderTypeChart.vue new file mode 100644 index 0000000..546d74e --- /dev/null +++ b/src/pages/product/component/OrderTypeChart.vue @@ -0,0 +1,51 @@ + + + diff --git a/src/pages/product/component/ProductFulfillmentRankChart.vue b/src/pages/product/component/ProductFulfillmentRankChart.vue new file mode 100644 index 0000000..b0ed39b --- /dev/null +++ b/src/pages/product/component/ProductFulfillmentRankChart.vue @@ -0,0 +1,57 @@ + + + diff --git a/src/pages/product/component/ProductFulfillmentTrendChart.vue b/src/pages/product/component/ProductFulfillmentTrendChart.vue new file mode 100644 index 0000000..f50dd60 --- /dev/null +++ b/src/pages/product/component/ProductFulfillmentTrendChart.vue @@ -0,0 +1,62 @@ + + + diff --git a/src/pages/product/component/ProductSalesRankChart.vue b/src/pages/product/component/ProductSalesRankChart.vue new file mode 100644 index 0000000..a7be842 --- /dev/null +++ b/src/pages/product/component/ProductSalesRankChart.vue @@ -0,0 +1,51 @@ + + + diff --git a/src/pages/product/component/ProductSalesTrendChart.vue b/src/pages/product/component/ProductSalesTrendChart.vue new file mode 100644 index 0000000..5cc8335 --- /dev/null +++ b/src/pages/product/component/ProductSalesTrendChart.vue @@ -0,0 +1,62 @@ + + + diff --git a/src/pages/product/component/ProductStockHealthTable.vue b/src/pages/product/component/ProductStockHealthTable.vue new file mode 100644 index 0000000..ee84c72 --- /dev/null +++ b/src/pages/product/component/ProductStockHealthTable.vue @@ -0,0 +1,153 @@ + + + + + diff --git a/src/pages/product/component/ProductStockMovementChart.vue b/src/pages/product/component/ProductStockMovementChart.vue new file mode 100644 index 0000000..3324070 --- /dev/null +++ b/src/pages/product/component/ProductStockMovementChart.vue @@ -0,0 +1,70 @@ + + + diff --git a/src/pages/product/component/chartUtils.ts b/src/pages/product/component/chartUtils.ts new file mode 100644 index 0000000..9c6fa88 --- /dev/null +++ b/src/pages/product/component/chartUtils.ts @@ -0,0 +1,215 @@ +import type { EChartsOption } from "echarts"; + +export const chartColorList = ["#2563eb", "#0f766e", "#d97706", "#db2777", "#7c3aed", "#0891b2"]; + +export function buildAxisChartOption(labelList: string[]): EChartsOption { + return { + color: chartColorList, + grid: { + top: 48, + left: 12, + right: 12, + bottom: 12, + containLabel: true + }, + legend: { + top: 0, + icon: "roundRect", + textStyle: { + color: "#52607a" + } + }, + tooltip: { + trigger: "axis", + confine: true + }, + xAxis: { + type: "category", + data: labelList, + axisTick: { + show: false + }, + axisLabel: { + color: "#52607a", + interval: "auto", + hideOverlap: true + } + }, + yAxis: { + type: "value", + axisLabel: { + color: "#52607a", + interval: "auto", + hideOverlap: true + }, + splitLine: { + lineStyle: { + color: "rgba(148, 163, 184, .2)" + } + } + } + }; +} + +export function buildHorizontalBarOption(categoryList: string[]): EChartsOption { + return { + color: chartColorList, + grid: { + top: 12, + left: 8, + right: 18, + bottom: 12, + containLabel: true + }, + legend: { + top: 0, + icon: "roundRect", + textStyle: { + color: "#52607a" + } + }, + tooltip: { + trigger: "axis", + axisPointer: { + type: "shadow" + }, + confine: true + }, + xAxis: { + type: "value", + axisLabel: { + color: "#52607a" + }, + splitLine: { + lineStyle: { + color: "rgba(148, 163, 184, .2)" + } + } + }, + yAxis: { + type: "category", + inverse: true, + data: categoryList, + axisTick: { + show: false + }, + axisLabel: { + color: "#52607a", + width: 92, + overflow: "truncate" + } + } + }; +} + +export function buildPieChartOption(): EChartsOption { + return { + color: chartColorList, + legend: { + bottom: 0, + icon: "circle", + textStyle: { + color: "#52607a" + } + }, + tooltip: { + trigger: "item", + confine: true + }, + series: [ + { + type: "pie", + radius: ["0%", "58%"], + center: ["50%", "45%"], + avoidLabelOverlap: true, + label: { + color: "#52607a", + formatter: "{b} {c}" + }, + labelLine: { + length: 12, + length2: 8 + } + } + ] + }; +} + +export function buildDateTextList(beginText: string, endText: string): string[] { + const begin = getDateStart(parseDateTime(beginText)); + const end = getDateStart(parseDateTime(endText)); + const list: string[] = []; + for (let item = begin; item <= end; item = addDays(item, 1)) { + list.push(formatDate(item)); + } + return list; +} + +export function toUnixRange(dateRange: readonly [string, string]): { beginAt: number; endAt: number } { + const end = parseDateTime(dateRange[1]); + if (end.getHours() === 23 && end.getMinutes() === 59) { + end.setSeconds(59, 999); + } + return { + beginAt: parseDateTime(dateRange[0]).getTime(), + endAt: end.getTime() + }; +} + +export function getDailyItem(list: T[], dateText: string): T | undefined { + return list.find(item => item.dateValue === toDateValue(dateText)); +} + +export function getNumber(value?: number): number { + return typeof value === "number" && Number.isFinite(value) ? value : 0; +} + +export function sumNumber(list: T[], getter: (item: T) => number | undefined): number { + return list.reduce((sum, item) => sum + getNumber(getter(item)), 0); +} + +export function formatQuantity(value?: number): string { + return getNumber(value).toLocaleString("zh-CN", { + maximumFractionDigits: 4 + }); +} + +export function formatMoney(value?: number): string { + return `¥${(getNumber(value) / 100).toLocaleString("zh-CN", { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + })}`; +} + +export function getDateStart(date: Date): Date { + return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0); +} + +export function getDateEnd(date: Date): Date { + return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999); +} + +function parseDateTime(value: string): Date { + const [dateText, timeText = "00:00"] = value.split(" "); + const [year, month, day] = dateText.split("-").map(Number); + const [hour, minute] = timeText.split(":").map(Number); + return new Date(year, month - 1, day, hour, minute, 0, 0); +} + +function addDays(date: Date, days: number): Date { + const value = new Date(date); + value.setDate(value.getDate() + days); + return value; +} + +function formatDate(date: Date): string { + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; +} + +function toDateValue(dateText: string): number { + return Number(dateText.replaceAll("-", "")); +} + +function pad(value: number): string { + return String(value).padStart(2, "0"); +} diff --git a/src/router/routes/chartRoutes.ts b/src/router/routes/chartRoutes.ts index cda6f80..a8644da 100644 --- a/src/router/routes/chartRoutes.ts +++ b/src/router/routes/chartRoutes.ts @@ -1,6 +1,34 @@ import type { RouteRecordRaw } from "vue-router"; const chartRoutes: RouteRecordRaw[] = [ + { + path: "/management/product-data-charts", + name: "ProductDataCharts", + component: () => import("@/pages/product/ProductDataCharts.vue"), + meta: { + depth: 3, + navBarVisible: true, + navBarCanBack: true, + navBarTitle: "产品经营图表", + tabBarVisible: false, + bodyBackground: "#F5F5F8", + darkBodyBackground: "#10161f" + } + }, + { + path: "/management/order-data-charts", + name: "OrderDataCharts", + component: () => import("@/pages/product/OrderDataCharts.vue"), + meta: { + depth: 3, + navBarVisible: true, + navBarCanBack: true, + navBarTitle: "客户经营图表", + tabBarVisible: false, + bodyBackground: "#F5F5F8", + darkBodyBackground: "#10161f" + } + }, { path: "/management/data-charts", name: "StoreDataCharts", diff --git a/src/types/GaoChart.ts b/src/types/GaoChart.ts new file mode 100644 index 0000000..7388999 --- /dev/null +++ b/src/types/GaoChart.ts @@ -0,0 +1,200 @@ +import type { ProductType } from "@/types/Product"; + +export type GaoProductChartReq = { + storeId?: string; + productId?: string; + categoryId?: string; + productType?: ProductType; + beginAt?: number; + endAt?: number; + topN?: number; +}; + +export type GaoSalesOrderChartReq = { + storeId?: string; + customerId?: string; + orderType?: "PACKAGE" | "DIRECT_PRODUCT"; + status?: "ACTIVE" | "FULFILLED" | "CANCELLED"; + paymentStatus?: "UNPAID" | "PARTIALLY_PAID" | "PAID"; + beginAt: number; + endAt: number; + topN?: number; +}; + +export type GaoProductSalesDailyStatView = { + dateValue?: number; + orderCount?: number; + directQuantity?: number; + packageQuantity?: number; + directSaleAmount?: number; +}; + +export type GaoProductSalesRankView = { + productId?: string; + productName?: string; + orderCount?: number; + directQuantity?: number; + packageQuantity?: number; + directSaleAmount?: number; +}; + +export type GaoProductSkuUnitSalesRankView = { + productId?: string; + productName?: string; + skuId?: string; + skuUnitId?: string; + skuName?: string; + specSummary?: string; + unitName?: string; + orderCount?: number; + quantity?: number; + saleAmount?: number; +}; + +export type GaoProductFulfillmentDailyStatView = { + dateValue?: number; + directQuantity?: number; + packageQuantity?: number; + baseQuantity?: number; +}; + +export type GaoProductFulfillmentRankView = { + productId?: string; + productName?: string; + orderCount?: number; + directQuantity?: number; + packageQuantity?: number; + reservedQuantity?: number; + fulfilledQuantity?: number; + availableQuantity?: number; +}; + +export type GaoProductStockMovementDailyStatView = { + dateValue?: number; + inQuantity?: number; + outQuantity?: number; + netOnHandQuantity?: number; + reserveQuantity?: number; + releaseQuantity?: number; +}; + +export type GaoProductStockHealthView = { + productId?: string; + productName?: string; + categoryId?: string | null; + categoryName?: string; + productType?: ProductType; + onHandQuantity?: number; + reservedQuantity?: number; + availableQuantity?: number; + minStockQuantity?: number; + lowStock?: number; +}; + +export type GaoProductTypeStatView = { + productType?: ProductType; + productCount?: number; +}; + +export type GaoProductCategoryStatView = { + categoryId?: string | null; + categoryName?: string; + productCount?: number; +}; + +export type GaoSalesOrderDailyStatView = { + dateValue?: number; + orderCount?: number; + saleAmount?: number; + paidAmount?: number; + pendingPaidAmount?: number; + refundedAmount?: number; + netPaidAmount?: number; +}; + +export type GaoSalesOrderTypeStatView = { + orderType?: "PACKAGE" | "DIRECT_PRODUCT"; + orderCount?: number; + saleAmount?: number; + paidAmount?: number; + refundedAmount?: number; + netPaidAmount?: number; +}; + +export type GaoSalesOrderPaymentStatView = { + paymentStatus?: "UNPAID" | "PARTIALLY_PAID" | "PAID"; + orderCount?: number; + saleAmount?: number; + paidAmount?: number; + pendingPaidAmount?: number; + unpaidAmount?: number; + refundedAmount?: number; +}; + +export type GaoSalesOrderStatusStatView = { + status?: "ACTIVE" | "FULFILLED" | "CANCELLED"; + orderCount?: number; + saleAmount?: number; + paidAmount?: number; + refundedAmount?: number; +}; + +export type GaoSalesOrderReceivableStatView = { + orderCount?: number; + saleAmount?: number; + paidAmount?: number; + pendingPaidAmount?: number; + unpaidAmount?: number; + refundedAmount?: number; + netPaidAmount?: number; +}; + +export type GaoCustomerOrderRankView = { + customerId?: string; + customerCode?: string; + customerName?: string; + orderCount?: number; + saleAmount?: number; + paidAmount?: number; + refundedAmount?: number; + netPaidAmount?: number; +}; + +export type GaoCustomerOrderProductRankView = { + productId?: string; + productName?: string; + orderCount?: number; + directQuantity?: number; + packageQuantity?: number; + reservedQuantity?: number; + fulfilledQuantity?: number; + availableQuantity?: number; + directSaleAmount?: number; +}; + +export type GaoSalesOrderConsumptionDailyStatView = { + dateValue?: number; + recordCount?: number; + amount?: number; + cumulativeAmount?: number; +}; + +export type GaoSalesOrderCalendarStatView = { + dateValue?: number; + orderCount?: number; + saleAmount?: number; + netPaidAmount?: number; +}; + +export type GaoCustomerRepeatDailyStatView = { + dateValue?: number; + customerCount?: number; + repeatCustomerCount?: number; + repeatOrderCount?: number; +}; + +export type GaoCustomerRepeatIntervalStatView = { + intervalDays?: number; + customerCount?: number; + orderCount?: number; +};