add product/order chart

This commit is contained in:
Timi
2026-09-06 23:20:26 +08:00
parent 0b01a54eac
commit 9841723177
20 changed files with 2236 additions and 0 deletions
@@ -0,0 +1,51 @@
<template>
<gao-chart-card
title="客户净收款排行"
caption="排除已取消订单,前 10"
:option="chartOption"
:loading="loading"
:has-data="list.length > 0"
height="17rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import type { GaoCustomerOrderRankView } from "@/types/GaoChart";
import { buildHorizontalBarOption, getNumber } from "@/pages/product/component/chartUtils";
defineOptions({
name: "CustomerOrderRankChart"
});
const props = defineProps<{
list: GaoCustomerOrderRankView[];
loading: boolean;
}>();
const chartOption = computed<EChartsOption>(() => {
const list = props.list.slice(0, 10);
return {
...buildHorizontalBarOption(list.map(getCustomerName)),
legend: { show: false },
xAxis: {
type: "value",
axisLabel: {
color: "#52607a",
formatter: (value: number) => `${value.toLocaleString("zh-CN")} 元`
},
splitLine: { lineStyle: { color: "rgba(148, 163, 184, .2)" } }
},
series: [{
name: "净收款",
type: "bar",
data: list.map(item => getNumber(item.netPaidAmount) / 100)
}]
};
});
function getCustomerName(item: GaoCustomerOrderRankView): string {
return item.customerName || item.customerCode || "未命名客户";
}
</script>
@@ -0,0 +1,113 @@
<template>
<section class="chart-section">
<div class="title-row">
<h2 v-text="title" />
<span v-if="caption" class="caption" v-text="caption" />
</div>
<div ref="chartElement" class="chart" :style="{ height }" />
<div v-if="loading" class="state">
<t-loading size="1.5rem" text="加载中" />
</div>
<div v-else-if="!hasData" class="state">
<t-empty description="暂无数据" />
</div>
<chart-range-slider
v-if="chartShowSlider"
:model-value="sliderValue"
:max="maxSliderValue"
:min-range="5"
:label="rangeText"
@change="handleSliderChange"
/>
</section>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import ChartRangeSlider from "@/components/ChartRangeSlider.vue";
import { useStoreChart, useStoreChartWindow } from "@/pages/store/component/useStoreChart";
type ChartOptionBuilder = (visibleDateTextList: string[]) => EChartsOption;
const props = withDefaults(defineProps<{
caption?: string;
dateTextList?: string[];
height?: string;
hasData?: boolean;
loading?: boolean;
option: EChartsOption | ChartOptionBuilder;
title: string;
}>(), {
caption: "",
dateTextList: () => [],
height: "16rem",
hasData: true,
loading: false
});
const dateTextList = computed(() => props.dateTextList);
const {
rangeText,
showSlider: chartShowSlider,
sliderValue,
maxSliderValue,
visibleDateTextList,
handleSliderChange
} = useStoreChartWindow(dateTextList);
const option = computed<EChartsOption>(() => typeof props.option === "function"
? props.option(visibleDateTextList.value)
: props.option);
const { chartElement } = useStoreChart(option);
</script>
<style scoped lang="less">
.chart-section {
padding: 1rem;
position: relative;
background: rgba(255, 255, 255, .92);
box-shadow: var(--td-shadow-1);
border-radius: 1.25rem;
.title-row {
gap: .5rem;
display: flex;
align-items: baseline;
justify-content: space-between;
}
h2 {
color: #0f172a;
margin: 0 0 .875rem 0;
font-size: 1rem;
font-weight: 700;
}
.caption {
color: #94a3b8;
font-size: .6875rem;
}
.chart {
width: 100%;
}
.state {
inset: 3.5rem 0 0 0;
display: flex;
position: absolute;
align-items: center;
justify-content: center;
}
}
:global(html.theme-dark) {
.chart-section {
background: rgba(15, 23, 42, .88);
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
h2 {
color: #e2e8f0;
}
}
}
</style>
@@ -0,0 +1,81 @@
<template>
<gao-chart-card
title="订单每日趋势"
caption="金额已换算为元"
:date-text-list="dateTextList"
:option="buildOption"
:loading="loading"
:has-data="list.length > 0"
height="18rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import type { GaoSalesOrderDailyStatView } from "@/types/GaoChart";
import { buildAxisChartOption, getDailyItem, getNumber } from "@/pages/product/component/chartUtils";
defineOptions({
name: "OrderDailyChart"
});
const props = defineProps<{
dateTextList: string[];
list: GaoSalesOrderDailyStatView[];
loading: boolean;
}>();
function buildOption(visibleDateTextList: string[]): EChartsOption {
const labelList = visibleDateTextList.map(item => item.slice(5));
return {
...buildAxisChartOption(labelList),
xAxis: {
type: "category",
data: labelList,
axisTick: { show: false },
axisLabel: { color: "#52607a", interval: "auto", hideOverlap: true }
},
yAxis: [
{
type: "value",
name: "订单",
axisLabel: { color: "#52607a" },
splitLine: { lineStyle: { color: "rgba(148, 163, 184, .2)" } }
},
{
type: "value",
name: "元",
axisLabel: {
color: "#52607a",
formatter: (value: number) => value.toLocaleString("zh-CN")
},
splitLine: { show: false }
}
],
series: [
{
name: "订单数",
type: "bar",
yAxisIndex: 0,
data: visibleDateTextList.map(dateText => getNumber(getDailyItem(props.list, dateText)?.orderCount))
},
{
name: "成交金额",
type: "line",
yAxisIndex: 1,
smooth: true,
data: visibleDateTextList.map(dateText => getNumber(getDailyItem(props.list, dateText)?.saleAmount) / 100)
},
{
name: "净收款",
type: "line",
yAxisIndex: 1,
smooth: true,
data: visibleDateTextList.map(dateText => getNumber(getDailyItem(props.list, dateText)?.netPaidAmount) / 100)
}
]
};
}
</script>
@@ -0,0 +1,43 @@
<template>
<gao-chart-card
title="付款状态分布"
caption="当前订单快照"
:option="chartOption"
:loading="loading"
:has-data="list.length > 0"
height="17rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import type { GaoSalesOrderPaymentStatView } from "@/types/GaoChart";
import { buildHorizontalBarOption, getNumber } from "@/pages/product/component/chartUtils";
defineOptions({
name: "OrderPaymentChart"
});
const props = defineProps<{
list: GaoSalesOrderPaymentStatView[];
loading: boolean;
}>();
const chartOption = computed<EChartsOption>(() => {
const list = props.list;
return {
...buildHorizontalBarOption(list.map(item => getPaymentStatusText(item.paymentStatus))),
legend: { show: false },
series: [{
name: "订单数",
type: "bar",
data: list.map(item => getNumber(item.orderCount))
}]
};
});
function getPaymentStatusText(value?: string): string {
return ({ UNPAID: "未付款", PARTIALLY_PAID: "部分付款", PAID: "已付款" } as Record<string, string>)[value || ""] || "未知状态";
}
</script>
@@ -0,0 +1,123 @@
<template>
<section class="receivable-section">
<div class="title-row">
<h2 v-text="'应收构成'" />
<span class="caption" v-text="'时间范围内创建订单的当前快照'" />
</div>
<div class="receivable-list">
<div v-for="item in detailList" :key="item.label" class="receivable-item">
<span v-text="item.label" />
<strong v-text="item.value" />
</div>
</div>
</section>
</template>
<script setup lang="ts">
import type { GaoSalesOrderReceivableStatView } from "@/types/GaoChart";
import { formatMoney } from "@/pages/product/component/chartUtils";
type DetailItem = {
label: string;
value: string;
};
defineOptions({
name: "OrderReceivableCard"
});
const props = defineProps<{
receivable: GaoSalesOrderReceivableStatView;
}>();
const detailList = computed<DetailItem[]>(() => [
{ label: "有效收款", value: formatMoney(props.receivable.paidAmount) },
{ label: "待核销", value: formatMoney(props.receivable.pendingPaidAmount) },
{ label: "未收", value: formatMoney(props.receivable.unpaidAmount) },
{ label: "有效退款", value: formatMoney(props.receivable.refundedAmount) },
{ label: "净收款", value: formatMoney(props.receivable.netPaidAmount) }
]);
</script>
<style scoped lang="less">
.receivable-section {
padding: 1rem;
background: rgba(255, 255, 255, .92);
box-shadow: var(--td-shadow-1);
border-radius: 1.25rem;
.title-row {
gap: .5rem;
display: flex;
align-items: baseline;
justify-content: space-between;
}
h2 {
color: #0f172a;
margin: 0 0 .875rem;
font-size: 1rem;
font-weight: 700;
}
.caption {
color: #94a3b8;
font-size: .6875rem;
}
.receivable-list {
gap: .5rem;
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.receivable-item {
padding: .625rem .5rem;
background: rgba(241, 245, 249, .8);
border-radius: .75rem;
span {
color: #64748b;
display: block;
font-size: .6875rem;
}
strong {
color: #0f172a;
margin-top: .25rem;
display: block;
font-size: .8125rem;
font-weight: 700;
}
}
}
:global(html.theme-dark) {
.receivable-section {
background: rgba(15, 23, 42, .88);
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
h2 {
color: #e2e8f0;
}
.receivable-item {
background: rgba(30, 41, 59, .88);
span {
color: #94a3b8;
}
strong {
color: #e2e8f0;
}
}
}
}
@media (max-width: 42rem) {
.receivable-list {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>
@@ -0,0 +1,43 @@
<template>
<gao-chart-card
title="订单状态分布"
caption="当前订单快照"
:option="chartOption"
:loading="loading"
:has-data="list.length > 0"
height="17rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import type { GaoSalesOrderStatusStatView } from "@/types/GaoChart";
import { buildHorizontalBarOption, getNumber } from "@/pages/product/component/chartUtils";
defineOptions({
name: "OrderStatusChart"
});
const props = defineProps<{
list: GaoSalesOrderStatusStatView[];
loading: boolean;
}>();
const chartOption = computed<EChartsOption>(() => {
const list = props.list;
return {
...buildHorizontalBarOption(list.map(item => getOrderStatusText(item.status))),
legend: { show: false },
series: [{
name: "订单数",
type: "bar",
data: list.map(item => getNumber(item.orderCount))
}]
};
});
function getOrderStatusText(value?: string): string {
return ({ ACTIVE: "进行中", FULFILLED: "已完成", CANCELLED: "已取消" } as Record<string, string>)[value || ""] || "未知状态";
}
</script>
@@ -0,0 +1,51 @@
<template>
<gao-chart-card
title="订单类型分布"
caption="按订单数量"
:option="chartOption"
:loading="loading"
:has-data="list.length > 0"
height="17rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import type { GaoSalesOrderTypeStatView } from "@/types/GaoChart";
import { buildPieChartOption, getNumber } from "@/pages/product/component/chartUtils";
defineOptions({
name: "OrderTypeChart"
});
const props = defineProps<{
list: GaoSalesOrderTypeStatView[];
loading: boolean;
}>();
const chartOption = computed<EChartsOption>(() => ({
...buildPieChartOption(),
legend: {
bottom: 0,
icon: "circle",
textStyle: { color: "#52607a" }
},
series: [
{
type: "pie",
radius: ["0%", "58%"],
center: ["50%", "44%"],
label: { color: "#52607a", formatter: "{b} {c} 单" },
data: props.list.map(item => ({
name: getOrderTypeText(item.orderType),
value: getNumber(item.orderCount)
}))
}
]
}));
function getOrderTypeText(value?: string): string {
return ({ PACKAGE: "套餐订单", DIRECT_PRODUCT: "直接产品" } as Record<string, string>)[value || ""] || "未知类型";
}
</script>
@@ -0,0 +1,57 @@
<template>
<gao-chart-card
title="履约进度排行"
caption="购买数量的当前履约快照"
:option="chartOption"
:loading="loading"
:has-data="list.length > 0"
height="20rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import type { GaoProductFulfillmentRankView } from "@/types/GaoChart";
import { buildHorizontalBarOption, getNumber } from "@/pages/product/component/chartUtils";
defineOptions({
name: "ProductFulfillmentRankChart"
});
const props = defineProps<{
list: GaoProductFulfillmentRankView[];
loading: boolean;
}>();
const chartOption = computed<EChartsOption>(() => {
const list = props.list.slice(0, 10);
return {
...buildHorizontalBarOption(list.map(getProductName)),
series: [
{
name: "待履约",
type: "bar",
stack: "progress",
data: list.map(item => getNumber(item.reservedQuantity))
},
{
name: "已履约",
type: "bar",
stack: "progress",
data: list.map(item => getNumber(item.fulfilledQuantity))
},
{
name: "可履约",
type: "bar",
stack: "progress",
data: list.map(item => getNumber(item.availableQuantity))
}
]
};
});
function getProductName(item: GaoProductFulfillmentRankView): string {
return item.productName || "未命名产品";
}
</script>
@@ -0,0 +1,62 @@
<template>
<gao-chart-card
title="每日履约趋势"
caption="冲正数量允许为负值"
:date-text-list="dateTextList"
:option="buildOption"
:loading="loading"
:has-data="list.length > 0"
height="17rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import type { GaoProductFulfillmentDailyStatView } from "@/types/GaoChart";
import { buildAxisChartOption, getDailyItem, getNumber } from "@/pages/product/component/chartUtils";
defineOptions({
name: "ProductFulfillmentTrendChart"
});
const props = defineProps<{
dateTextList: string[];
list: GaoProductFulfillmentDailyStatView[];
loading: boolean;
}>();
function buildOption(visibleDateTextList: string[]): EChartsOption {
const labelList = visibleDateTextList.map(item => item.slice(5));
return {
...buildAxisChartOption(labelList),
xAxis: {
type: "category",
data: labelList,
axisTick: { show: false },
axisLabel: { color: "#52607a", interval: "auto", hideOverlap: true }
},
series: [
{
name: "直接履约",
type: "bar",
stack: "fulfillment",
data: visibleDateTextList.map(dateText => getNumber(getDailyItem(props.list, dateText)?.directQuantity))
},
{
name: "套餐履约",
type: "bar",
stack: "fulfillment",
data: visibleDateTextList.map(dateText => getNumber(getDailyItem(props.list, dateText)?.packageQuantity))
},
{
name: "基础单位变化",
type: "line",
smooth: true,
data: visibleDateTextList.map(dateText => getNumber(getDailyItem(props.list, dateText)?.baseQuantity))
}
]
};
}
</script>
@@ -0,0 +1,51 @@
<template>
<gao-chart-card
title="产品销售排行"
caption="按销量取前 10"
:option="chartOption"
:loading="loading"
:has-data="list.length > 0"
height="20rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import type { GaoProductSalesRankView } from "@/types/GaoChart";
import { buildHorizontalBarOption, getNumber } from "@/pages/product/component/chartUtils";
defineOptions({
name: "ProductSalesRankChart"
});
const props = defineProps<{
list: GaoProductSalesRankView[];
loading: boolean;
}>();
const chartOption = computed<EChartsOption>(() => {
const list = props.list.slice(0, 10);
return {
...buildHorizontalBarOption(list.map(getProductName)),
series: [
{
name: "直接销量",
type: "bar",
stack: "quantity",
data: list.map(item => getNumber(item.directQuantity))
},
{
name: "套餐权益",
type: "bar",
stack: "quantity",
data: list.map(item => getNumber(item.packageQuantity))
}
]
};
});
function getProductName(item: GaoProductSalesRankView): string {
return item.productName || "未命名产品";
}
</script>
@@ -0,0 +1,62 @@
<template>
<gao-chart-card
title="产品销售趋势"
caption="直接产品与套餐权益分开展示"
:date-text-list="dateTextList"
:option="buildOption"
:loading="loading"
:has-data="list.length > 0"
height="17rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import type { GaoProductSalesDailyStatView } from "@/types/GaoChart";
import { buildAxisChartOption, getDailyItem, getNumber } from "@/pages/product/component/chartUtils";
defineOptions({
name: "ProductSalesTrendChart"
});
const props = defineProps<{
dateTextList: string[];
list: GaoProductSalesDailyStatView[];
loading: boolean;
}>();
function buildOption(visibleDateTextList: string[]): EChartsOption {
const labelList = visibleDateTextList.map(item => item.slice(5));
return {
...buildAxisChartOption(labelList),
xAxis: {
type: "category",
data: labelList,
axisTick: { show: false },
axisLabel: { color: "#52607a", interval: "auto", hideOverlap: true }
},
series: [
{
name: "直接销量",
type: "bar",
stack: "quantity",
data: visibleDateTextList.map(dateText => getNumber(getDailyItem(props.list, dateText)?.directQuantity))
},
{
name: "套餐权益",
type: "bar",
stack: "quantity",
data: visibleDateTextList.map(dateText => getNumber(getDailyItem(props.list, dateText)?.packageQuantity))
},
{
name: "订单数",
type: "line",
smooth: true,
data: visibleDateTextList.map(dateText => getNumber(getDailyItem(props.list, dateText)?.orderCount))
}
]
};
}
</script>
@@ -0,0 +1,153 @@
<template>
<section class="table-section">
<div class="title-row">
<h2 v-text="'库存健康度'" />
<span class="caption" v-text="'低库存优先'" />
</div>
<div v-if="loading" class="table-state"><t-loading size="1.5rem" text="加载中" /></div>
<div v-else-if="!list.length" class="table-state"><t-empty description="暂无库存数据" /></div>
<div v-else class="table-wrap">
<table>
<thead>
<tr>
<th v-text="'产品'" />
<th v-text="'可用库存'" />
<th v-text="'预占'" />
<th v-text="'最低库存'" />
<th v-text="'状态'" />
</tr>
</thead>
<tbody>
<tr v-for="item in sortedList" :key="item.productId">
<td>
<div class="product-name" v-text="item.productName || '未命名产品'" />
<div class="product-type" v-text="getProductTypeText(item.productType)" />
</td>
<td class="number" v-text="formatQuantity(item.availableQuantity)" />
<td class="number" v-text="formatQuantity(item.reservedQuantity)" />
<td class="number" v-text="formatQuantity(item.minStockQuantity)" />
<td>
<t-tag v-if="item.lowStock === 1" theme="danger" variant="light" content="低库存" />
<t-tag v-else theme="success" variant="light" content="正常" />
</td>
</tr>
</tbody>
</table>
</div>
</section>
</template>
<script setup lang="ts">
import type { GaoProductStockHealthView } from "@/types/GaoChart";
import { formatQuantity, getNumber } from "@/pages/product/component/chartUtils";
defineOptions({
name: "ProductStockHealthTable"
});
const props = defineProps<{
list: GaoProductStockHealthView[];
loading: boolean;
}>();
const sortedList = computed(() => [...props.list].sort((left, right) => {
const lowStockDiff = getNumber(right.lowStock) - getNumber(left.lowStock);
return lowStockDiff || getNumber(left.availableQuantity) - getNumber(right.availableQuantity);
}));
function getProductTypeText(value?: string): string {
return ({ SERVICE: "服务", GOODS: "商品", CONSUMABLE: "耗材" } as Record<string, string>)[value || ""] || "未分类";
}
</script>
<style scoped lang="less">
.table-section {
padding: 1rem;
background: rgba(255, 255, 255, .92);
box-shadow: var(--td-shadow-1);
border-radius: 1.25rem;
.title-row {
gap: .5rem;
display: flex;
align-items: baseline;
justify-content: space-between;
}
h2 {
color: #0f172a;
margin: 0 0 .875rem;
font-size: 1rem;
font-weight: 700;
}
.caption {
color: #94a3b8;
font-size: .6875rem;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: .75rem;
white-space: nowrap;
}
th {
color: #94a3b8;
padding: .5rem .375rem;
text-align: left;
font-weight: 500;
}
td {
color: #334155;
padding: .625rem .375rem;
border-top: 1px solid rgba(148, 163, 184, .16);
}
.number {
text-align: right;
font-variant-numeric: tabular-nums;
}
.product-name {
max-width: 7rem;
overflow: hidden;
text-overflow: ellipsis;
}
.product-type {
color: #94a3b8;
margin-top: .125rem;
font-size: .6875rem;
}
.table-state {
padding: 1.5rem 0;
display: flex;
align-items: center;
justify-content: center;
}
}
:global(html.theme-dark) {
.table-section {
background: rgba(15, 23, 42, .88);
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
h2 {
color: #e2e8f0;
}
td {
color: #cbd5e1;
border-top-color: rgba(148, 163, 184, .2);
}
}
}
</style>
@@ -0,0 +1,70 @@
<template>
<gao-chart-card
title="库存变动"
caption="预占与释放不计入实际出入库"
:date-text-list="dateTextList"
:option="buildOption"
:loading="loading"
:has-data="list.length > 0"
height="17rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import type { GaoProductStockMovementDailyStatView } from "@/types/GaoChart";
import { buildAxisChartOption, getDailyItem, getNumber } from "@/pages/product/component/chartUtils";
defineOptions({
name: "ProductStockMovementChart"
});
const props = defineProps<{
dateTextList: string[];
list: GaoProductStockMovementDailyStatView[];
loading: boolean;
}>();
function buildOption(visibleDateTextList: string[]): EChartsOption {
const labelList = visibleDateTextList.map(item => item.slice(5));
return {
...buildAxisChartOption(labelList),
xAxis: {
type: "category",
data: labelList,
axisTick: { show: false },
axisLabel: { color: "#52607a", interval: "auto", hideOverlap: true }
},
series: [
{
name: "入库",
type: "bar",
data: visibleDateTextList.map(dateText => getNumber(getDailyItem(props.list, dateText)?.inQuantity))
},
{
name: "出库",
type: "bar",
data: visibleDateTextList.map(dateText => getNumber(getDailyItem(props.list, dateText)?.outQuantity))
},
{
name: "库存净变化",
type: "line",
smooth: true,
data: visibleDateTextList.map(dateText => getNumber(getDailyItem(props.list, dateText)?.netOnHandQuantity))
},
{
name: "新增预占",
type: "line",
data: visibleDateTextList.map(dateText => getNumber(getDailyItem(props.list, dateText)?.reserveQuantity))
},
{
name: "释放预占",
type: "line",
data: visibleDateTextList.map(dateText => getNumber(getDailyItem(props.list, dateText)?.releaseQuantity))
}
]
};
}
</script>
+215
View File
@@ -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<T extends { dateValue?: number }>(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<T>(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");
}