add point chart

This commit is contained in:
Timi
2026-09-23 21:54:19 +08:00
parent f8574c387d
commit 3a9dbbb60b
14 changed files with 462 additions and 93 deletions
+26
View File
@@ -0,0 +1,26 @@
import { axios } from "timi-web-api";
import type { GaoPointChartReq, GaoPointDailyStatView, GaoPointRedemptionProductRankView } from "@/types/GaoChart";
import { getStoreChartCache } from "@/utils/storeChartCache";
import { withCurrentStoreId } from "@/utils/store";
const BASE_URI = "/gao/store/chart";
/** 查询按日汇总的获得积分、消耗积分及两者差值。 */
async function dailyTrend(req: GaoPointChartReq): Promise<GaoPointDailyStatView[]> {
return await requestList("point-daily-trend", "/point/ledger/daily", req);
}
/** 查询兑换积分最高的前 10 个产品,不扣除退回积分。 */
async function redemptionProductRank(req: GaoPointChartReq): Promise<GaoPointRedemptionProductRankView[]> {
return await requestList("point-redemption-product-rank", "/point/redemption/product/rank", req);
}
async function requestList<T>(name: string, path: string, req: GaoPointChartReq): Promise<T[]> {
const request = withCurrentStoreId(req);
return await getStoreChartCache(`gao-point:${name}:${JSON.stringify(request)}`, async () => await axios.post(`${BASE_URI}${path}`, request));
}
export default {
dailyTrend,
redemptionProductRank
};
+11
View File
@@ -0,0 +1,11 @@
import { axios } from "timi-web-api";
import type { Role } from "timi-web-api";
const BASE_URI = "/gao/role";
export const GaoRoleAPI = {
/** 查询登录用户所属门店内可管理的角色。 */
async managementList(): Promise<Role[]> {
return await axios.post(`${BASE_URI}/management/list`);
}
};
@@ -554,7 +554,6 @@ onUnmounted(() => navBarStore.clearRight());
.list-wrap {
gap: 1rem;
display: flex;
padding-top: 1rem;
flex-direction: column;
.date-summary {
+3 -26
View File
@@ -92,12 +92,10 @@ const router = useRouter();
const userStore = useUserStore();
const permissionStore = usePermissionSettingsStore();
const authorizationRoleList = ref<Role[]>([]);
const directRoleIdSet = ref(new Set<string>());
const manageableRoleIdSet = ref(new Set<string>());
const isSystemAdmin = computed(() => userStore.hasRoles(coreRoleCodes.admin));
const roleCount = computed(() => authorizationRoleList.value.length);
const permissionCount = computed(() => permissionStore.permissionList.length);
type RoleTreeItem = Pick<Role, "id" | "name" | "parentRoleId" | "protectedRole"> & { level: number };
type RoleTreeItem = Pick<Role, "id" | "name" | "parentRoleId" | "protectedRole" | "ownerType"> & { level: number };
const visibleRoleTree = computed<RoleTreeItem[]>(() => {
if (isSystemAdmin.value) {
@@ -147,36 +145,15 @@ function canAssignRole(role: Pick<Role, "protectedRole">): boolean {
return isSystemAdmin.value || role.protectedRole !== true;
}
function getRoleScopeLabel(role: Pick<Role, "id">): string {
const direct = directRoleIdSet.value.has(role.id);
const manageable = manageableRoleIdSet.value.has(role.id);
if (direct && manageable) {
return "直属且可管理";
}
return direct ? "直属角色" : "可管理角色";
function getRoleScopeLabel(role: Pick<Role, "ownerType">): string {
return isSystemAdmin.value ? "系统角色" : role.ownerType === "STORE" ? "本店角色" : "可管理角色";
}
// ---------- 页面数据 ----------
onMounted(async () => {
if (isSystemAdmin.value) {
await permissionStore.loadAll();
authorizationRoleList.value = permissionStore.roleList;
return;
}
await permissionStore.loadAll();
const [manageableRoleList, currentRoleList] = await Promise.all([
permissionStore.loadManageableRoles(),
permissionStore.loadCurrentRoleDetails()
]);
const roleMap = new Map<string, Role>();
for (const role of [...currentRoleList, ...manageableRoleList]) {
roleMap.set(role.id, role);
}
authorizationRoleList.value = Array.from(roleMap.values());
directRoleIdSet.value = new Set(currentRoleList.map(item => item.id).filter((id): id is string => Boolean(id)));
manageableRoleIdSet.value = new Set(manageableRoleList.map(item => item.id).filter((id): id is string => Boolean(id)));
});
</script>
@@ -186,7 +186,7 @@ function clearPermissionState(): void {
// ---------- 授权范围 ----------
async function loadAuthorizationScope(): Promise<void> {
candidateRoleList.value = isSystemAdmin.value ? permissionStore.roleList : await permissionStore.loadManageableRoles();
candidateRoleList.value = permissionStore.roleList;
}
onMounted(async () => {
+24 -16
View File
@@ -330,7 +330,7 @@ async function fillForm(): Promise<void> {
await permissionStore.loadAll();
await loadAuthorizationScope();
if (isCreateMode.value) {
parentRoleOptions.value = await permissionStore.loadCreateParentGrantableRoles();
parentRoleOptions.value = filterStoreParentRoles(await permissionStore.loadCreateParentGrantableRoles());
formModel.parentRoleId = parentRoleOptions.value[0]?.id || "";
return;
}
@@ -355,30 +355,38 @@ async function reloadCurrentRole(): Promise<void> {
childRoleIdList
});
parentRoleOptions.value = canEditParentRole.value
? await permissionStore.loadParentGrantableRoles(current.id)
? filterStoreParentRoles(await permissionStore.loadParentGrantableRoles(current.id))
: [];
if (current.parentRole?.id && !parentRoleOptions.value.some(item => item.id === current.parentRoleId)) {
const currentParent = current.parentRole;
const canKeepCurrentParent = isSystemAdmin.value || Boolean(currentParent && (
currentParent.moduleCode === moduleCode && currentParent.code === "STORE_MANAGER"
|| permissionStore.roleList.some(item => item.id === currentParent.id)
));
if (currentParent?.id && canKeepCurrentParent && !parentRoleOptions.value.some(item => item.id === current.parentRoleId)) {
parentRoleOptions.value.unshift({
...current.parentRole,
id: current.parentRole.id,
permissionIdList: current.parentRole.permissionIdList || current.parentRole.permissionList?.map(item => item.id) || [],
childRoleIdList: current.parentRole.childRoleIdList || current.parentRole.childRoleList?.map(item => item.id).filter((id): id is string => Boolean(id)) || [],
delegationPermissionIdList: current.parentRole.delegationPermissionIdList || current.parentRole.delegationPermissionList?.map(item => item.id) || []
...currentParent,
id: currentParent.id,
permissionIdList: currentParent.permissionIdList || currentParent.permissionList?.map(item => item.id) || [],
childRoleIdList: currentParent.childRoleIdList || currentParent.childRoleList?.map(item => item.id).filter((id): id is string => Boolean(id)) || [],
delegationPermissionIdList: currentParent.delegationPermissionIdList || currentParent.delegationPermissionList?.map(item => item.id) || []
});
}
}
async function loadAuthorizationScope(): Promise<void> {
candidateRoleList.value = isSystemAdmin.value ? permissionStore.roleList : await permissionStore.loadManageableRoles();
inheritanceRoleList.value = mergeRoleList(permissionStore.roleList, candidateRoleList.value);
/** 限制门店角色可选择的管理父级。 */
function filterStoreParentRoles(roleList: Role[]): Role[] {
if (isSystemAdmin.value) {
return roleList;
}
function mergeRoleList(...roleLists: Role[][]): Role[] {
const roleMap = new Map<string, Role>();
for (const role of roleLists.flat()) {
roleMap.set(role.id, role);
const storeRoleIdSet = new Set(permissionStore.roleList.map(item => item.id));
return roleList.filter(role => storeRoleIdSet.has(role.id)
|| (role.moduleCode === moduleCode && role.code === "STORE_MANAGER"));
}
return Array.from(roleMap.values());
async function loadAuthorizationScope(): Promise<void> {
candidateRoleList.value = permissionStore.roleList;
inheritanceRoleList.value = [...permissionStore.roleList];
}
// ---------- 保存 ----------
+7 -28
View File
@@ -53,7 +53,6 @@ const permissionStore = usePermissionSettingsStore();
const userStore = useUserStore();
const isRefreshing = ref(false);
const deletableRoleIdSet = ref(new Set<string>());
const directRoleIdSet = ref(new Set<string>());
// ---------- 顶部导航 ----------
@@ -80,9 +79,11 @@ const sortedRoles = computed<Array<Role & { level: number }>>(() => {
async function removeRole(id: string): Promise<void> {
try {
await permissionStore.removeRole(id);
} catch (error) {
await refreshAuthorizationScope();
await permissionStore.loadAll(true);
await refreshAuthorizationScope();
} catch (error) {
await permissionStore.loadAll(true);
await refreshAuthorizationScope();
Message.error(getErrorMessage(error, "角色删除失败"));
}
}
@@ -95,12 +96,7 @@ function canDeleteRole(role: Role): boolean {
}
function getRoleScopeLabel(role: Role): string {
const direct = directRoleIdSet.value.has(role.id);
const manageable = deletableRoleIdSet.value.has(role.id);
if (direct && manageable) {
return "直属且可管理";
}
return direct ? "直属角色" : "可管理角色";
return userStore.hasRoles(coreRoleCodes.admin) ? "系统角色" : role.ownerType === "STORE" ? "本店角色" : "可管理角色";
}
// ---------- 页面数据 ----------
@@ -121,9 +117,7 @@ onMounted(async () => {
if (canCreateRole.value) {
navBarStore.setRightText("添加", "var(--td-brand-color)", toCreate);
}
if (userStore.hasRoles(coreRoleCodes.admin)) {
await permissionStore.loadAll();
}
await refreshAuthorizationScope();
if (!displayRoleList.value.length) {
displayRoleList.value = getDisplayRoleList();
@@ -163,23 +157,8 @@ async function refreshAuthorizationScope(): Promise<void> {
deletableRoleIdSet.value = new Set(permissionStore.roleList.map(item => item.id));
return;
}
try {
const [manageableRoleList, currentRoleList] = await Promise.all([
permissionStore.loadManageableRoles(),
permissionStore.loadCurrentRoleDetails()
]);
const roleMap = new Map<string, Role>();
for (const role of [...currentRoleList, ...manageableRoleList]) {
roleMap.set(role.id, role);
}
displayRoleList.value = Array.from(roleMap.values());
directRoleIdSet.value = new Set(currentRoleList.map(item => item.id).filter((id): id is string => Boolean(id)));
deletableRoleIdSet.value = new Set(manageableRoleList.map(item => item.id).filter((id): id is string => Boolean(id)));
} catch {
directRoleIdSet.value = new Set<string>();
deletableRoleIdSet.value = new Set<string>();
}
displayRoleList.value = permissionStore.roleList;
deletableRoleIdSet.value = new Set(permissionStore.roleList.map(item => item.id));
}
</script>
@@ -217,7 +217,7 @@ function clearPermissionState(): void {
// ---------- 授权范围 ----------
async function loadAuthorizationScope(): Promise<void> {
candidateRoleList.value = isSystemAdmin.value ? permissionStore.roleList : await permissionStore.loadManageableRoles();
candidateRoleList.value = permissionStore.roleList;
}
onMounted(async () => {
@@ -584,6 +584,7 @@ onUnmounted(() => {
}
.list-wrap {
gap: 1rem;
display: flex;
flex-direction: column;
+11 -2
View File
@@ -15,11 +15,17 @@
<store-event-structure-chart v-entrance-transition="280" :date-text-list="dateTextList" />
</template>
<template v-else>
<template v-if="chartMode === 'customer'">
<store-customer-growth-chart v-entrance-transition="70" :date-text-list="dateTextList" />
<store-customer-age-chart v-entrance-transition="140" />
<store-customer-disease-word-cloud-chart v-entrance-transition="210" />
<store-customer-gender-chart v-entrance-transition="280" />
</template>
<template v-else>
<store-point-daily-trend-chart v-entrance-transition="70" :date-text-list="dateTextList" />
<store-point-redemption-rank-chart v-entrance-transition="140" :date-text-list="dateTextList" />
</template>
</template>
</div>
</template>
@@ -34,6 +40,8 @@ import StoreEventRecordCalendarSection from "@/pages/store/component/StoreEventR
import StoreEventRecordTrendChart from "@/pages/store/component/StoreEventRecordTrendChart.vue";
import StoreEventRecordNumberValueChart from "@/pages/store/component/StoreEventRecordNumberValueChart.vue";
import StoreEventStructureChart from "@/pages/store/component/StoreEventStructureChart.vue";
import StorePointDailyTrendChart from "@/pages/store/component/StorePointDailyTrendChart.vue";
import StorePointRedemptionRankChart from "@/pages/store/component/StorePointRedemptionRankChart.vue";
import { permissionCodes } from "@/utils/permission";
import type { EventRecordCalendarListPayload } from "@/components/EventRecordCalendar.vue";
@@ -41,14 +49,15 @@ defineOptions({
name: "StoreDataCharts"
});
type ChartMode = "event" | "customer";
type ChartMode = "event" | "customer" | "point";
// ---------- 图表切换 ----------
const chartMode = ref<ChartMode>("event");
const chartModeOptions = [
{ value: "event" as ChartMode, label: "事件登记图表" },
{ value: "customer" as ChartMode, label: "客户分析图表" }
{ value: "customer" as ChartMode, label: "客户分析图表" },
{ value: "point" as ChartMode, label: "积分分析图表" }
];
// ---------- 数据源 ----------
@@ -0,0 +1,225 @@
<template>
<section class="chart-section">
<div class="title">
<h2>每日积分变动趋势</h2>
</div>
<div ref="chartElement" class="chart" />
<div v-if="loading" class="center-tips">
<t-loading size="1.5rem" text="加载中" />
</div>
<chart-range-slider
v-if="showSlider"
:model-value="sliderValue"
:max="maxSliderValue"
:min-range="5"
:label="rangeText"
@change="handleSliderChange"
/>
</section>
</template>
<script setup lang="ts">
import { Time } from "timi-web";
import type { EChartsOption } from "echarts";
import GaoPointChartAPI from "@/api/GaoPointChartAPI";
import ChartRangeSlider from "@/components/ChartRangeSlider.vue";
import type { GaoPointDailyStatView } from "@/types/GaoChart";
import { buildStoreBaseChartOption, useStoreChart, useStoreChartWindow } from "@/pages/store/component/useStoreChart";
import { buildRestDayMarkArea, useRestDayCalendar } from "@/pages/store/component/useRestDayCalendar";
defineOptions({
name: "StorePointDailyTrendChart"
});
const props = defineProps<{
dateTextList: string[];
}>();
const dateTextList = computed(() => props.dateTextList);
const chartWidth = ref(window.innerWidth);
const statList = ref<GaoPointDailyStatView[]>([]);
const loading = ref(false);
let statRequestId = 0;
const { restDaySet } = useRestDayCalendar(dateTextList);
const {
rangeText,
showSlider,
sliderValue,
maxSliderValue,
visibleDateTextList,
handleSliderChange
} = useStoreChartWindow(dateTextList);
const chartOption = computed<EChartsOption>(() => {
const labelList = visibleDateTextList.value.map(item => item.slice(5));
return {
...buildStoreBaseChartOption(visibleDateTextList.value),
grid: {
top: 32,
left: 0,
right: 0,
bottom: 0,
containLabel: true
},
xAxis: {
type: "category",
data: labelList,
axisLine: { onZero: true },
axisTick: { show: true, length: 0, interval: 0 },
axisLabel: { color: "#52607a", interval: "auto", hideOverlap: true }
},
series: [
{
name: "获得积分",
type: "bar",
stack: "stock-movement",
markArea: buildRestDayMarkArea(visibleDateTextList.value, restDaySet.value, chartWidth.value),
data: visibleDateTextList.value.map(dateText => Math.abs(getNumber(getDailyItem(dateText)?.earnedPoints)))
},
{
name: "消耗积分",
type: "bar",
stack: "stock-movement",
data: visibleDateTextList.value.map(dateText => -Math.abs(getNumber(getDailyItem(dateText)?.consumedPoints)))
},
{
name: "获得与消耗差值",
type: "line",
smooth: true,
data: visibleDateTextList.value.map(dateText => getNumber(getDailyItem(dateText)?.netChangePoints))
}
]
};
});
const { chartElement } = useStoreChart(chartOption);
let resizeObserver: ResizeObserver | undefined;
watch(() => props.dateTextList, async () => {
await loadDailyStat();
}, {
deep: true,
immediate: true
});
onMounted(() => {
resizeObserver = new window.ResizeObserver(updateChartWidth);
if (chartElement.value) {
resizeObserver.observe(chartElement.value);
updateChartWidth();
}
});
onBeforeUnmount(() => {
resizeObserver?.disconnect();
resizeObserver = undefined;
});
async function loadDailyStat(): Promise<void> {
if (!props.dateTextList.length) {
statRequestId++;
statList.value = [];
loading.value = false;
return;
}
const requestId = ++statRequestId;
loading.value = true;
try {
const list = await GaoPointChartAPI.dailyTrend({
beginAt: Time.toTimestamp(getDateStart(props.dateTextList[0])),
endAt: Time.toTimestamp(getDateEnd(props.dateTextList[props.dateTextList.length - 1]))
});
if (requestId === statRequestId) {
statList.value = list;
}
} finally {
if (requestId === statRequestId) {
loading.value = false;
}
}
}
function getDailyItem(dateText: string): GaoPointDailyStatView | undefined {
const dateValue = Number(dateText.replaceAll("-", ""));
return statList.value.find(item => item.dateValue === dateValue);
}
function getNumber(value?: number): number {
return typeof value === "number" && Number.isFinite(value) ? value : 0;
}
function updateChartWidth(): void {
chartWidth.value = chartElement.value?.clientWidth || window.innerWidth;
}
function getDateStart(dateText: string): Date {
const [year, month, day] = dateText.split("-").map(Number);
return new Date(year, month - 1, day, 0, 0, 0, 0);
}
function getDateEnd(dateText: string): Date {
const [year, month, day] = dateText.split("-").map(Number);
return new Date(year, month - 1, day, 23, 59, 59, 999);
}
</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 {
gap: .5rem;
display: flex;
align-items: baseline;
justify-content: space-between;
h2 {
margin: 0 0 .875rem 0;
color: #0f172a;
font-size: 1rem;
font-weight: 700;
}
.caption {
color: #94a3b8;
font-size: .6875rem;
}
}
.chart {
width: 100%;
height: 17rem;
}
.center-tips {
top: 50%;
left: 0;
right: 0;
position: absolute;
display: flex;
justify-content: center;
transform: translateY(-50%);
pointer-events: none;
}
}
:global(html.theme-dark) {
.chart-section {
background: rgba(15, 23, 42, .88);
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
.title {
h2 {
color: #e2e8f0;
}
.caption {
color: #94a3b8;
}
}
}
}
</style>
@@ -0,0 +1,95 @@
<template>
<date-range-chart-card
title="积分兑换排行"
:option="chartOption"
:loading="loading"
height="20rem"
/>
</template>
<script setup lang="ts">
import { Time } from "timi-web";
import type { EChartsOption } from "echarts";
import GaoPointChartAPI from "@/api/GaoPointChartAPI";
import DateRangeChartCard from "@/components/DateRangeChartCard.vue";
import type { GaoPointRedemptionProductRankView } from "@/types/GaoChart";
import { buildHorizontalBarOption, getNumber } from "@/pages/product/component/chartUtils";
defineOptions({
name: "StorePointRedemptionRankChart"
});
const props = defineProps<{
dateTextList: string[];
}>();
const rankList = ref<GaoPointRedemptionProductRankView[]>([]);
const loading = ref(false);
let rankRequestId = 0;
const chartOption = computed<EChartsOption>(() => {
const list = rankList.value.slice(0, 10);
return {
...buildHorizontalBarOption(list.map(getProductName)),
series: [
{
name: "兑换积分",
type: "bar",
barMaxWidth: 20,
label: {
show: true,
position: "right",
color: "#52607a",
formatter: "{c}"
},
data: list.map(item => getNumber(item.pointCost))
}
]
};
});
watch(() => props.dateTextList, async () => {
await loadRankList();
}, {
deep: true,
immediate: true
});
async function loadRankList(): Promise<void> {
if (!props.dateTextList.length) {
rankRequestId++;
rankList.value = [];
loading.value = false;
return;
}
const requestId = ++rankRequestId;
loading.value = true;
try {
const list = await GaoPointChartAPI.redemptionProductRank({
beginAt: Time.toTimestamp(getDateStart(props.dateTextList[0])),
endAt: Time.toTimestamp(getDateEnd(props.dateTextList[props.dateTextList.length - 1]))
});
if (requestId === rankRequestId) {
rankList.value = list.slice(0, 10);
}
} finally {
if (requestId === rankRequestId) {
loading.value = false;
}
}
}
function getProductName(item: GaoPointRedemptionProductRankView): string {
return item.productName || "未命名产品";
}
function getDateStart(dateText: string): Date {
const [year, month, day] = dateText.split("-").map(Number);
return new Date(year, month - 1, day, 0, 0, 0, 0);
}
function getDateEnd(dateText: string): Date {
const [year, month, day] = dateText.split("-").map(Number);
return new Date(year, month - 1, day, 23, 59, 59, 999);
}
</script>
+30 -11
View File
@@ -1,6 +1,9 @@
import { defineStore } from "pinia";
import { PermissionAPI } from "timi-web-api/api/PermissionAPI";
import { RoleAPI } from "timi-web-api/api/RoleAPI";
import { useUserStore } from "timi-web-api";
import { GaoRoleAPI } from "@/api/GaoRoleAPI";
import { coreRoleCodes } from "@/utils/permission";
import type { ModuleCode, PermissionPayload, Role as UserRole } from "timi-web-api/types";
import type {
Permission,
@@ -58,6 +61,7 @@ function createEmptyPermission(): Permission {
}
export const usePermissionSettingsStore = defineStore("permission-settings", () => {
const userStore = useUserStore();
const roleList = ref<Role[]>([]);
const permissionList = ref<Permission[]>([]);
const manageableRoleList = ref<Role[]>([]);
@@ -72,6 +76,24 @@ export const usePermissionSettingsStore = defineStore("permission-settings", ()
return roots.flatMap(item => buildRoleTree(item.id, 0, new Set<string>()));
});
async function loadRoleList(): Promise<Role[]> {
if (!userStore.hasRoles(coreRoleCodes.admin)) {
return (await GaoRoleAPI.managementList()).map(normalizeRole);
}
const result = await RoleAPI.list({
index: 0,
size: pageSize,
equalsExample: {
moduleCode: moduleCode
},
orderMap: {
code: "ASC"
}
});
return result.list.map(normalizeRole);
}
// ---------- 基础数据 ----------
async function loadAll(force = false): Promise<void> {
@@ -87,16 +109,7 @@ export const usePermissionSettingsStore = defineStore("permission-settings", ()
loading.value = true;
loadTask = (async () => {
const [roleResult, permissionResult] = await Promise.all([
RoleAPI.list({
index: 0,
size: pageSize,
equalsExample: {
moduleCode: moduleCode
},
orderMap: {
code: "ASC"
}
}),
loadRoleList(),
PermissionAPI.list({
index: 0,
size: pageSize,
@@ -108,7 +121,10 @@ export const usePermissionSettingsStore = defineStore("permission-settings", ()
}
})
]);
roleList.value = roleResult.list.map(normalizeRole);
roleList.value = roleResult;
if (!userStore.hasRoles(coreRoleCodes.admin)) {
manageableRoleList.value = roleResult;
}
permissionList.value = permissionResult.list;
loaded.value = true;
})();
@@ -125,6 +141,9 @@ export const usePermissionSettingsStore = defineStore("permission-settings", ()
if (!id) {
return createEmptyRole();
}
if (!userStore.hasRoles(coreRoleCodes.admin) && !roleList.value.some(item => item.id === id)) {
throw new Error("角色不在本店可管理范围内");
}
const role = normalizeRole(await RoleAPI.detail(id));
upsertLocalRole(role);
+20
View File
@@ -11,6 +11,26 @@ export type GaoProductChartReq = {
topN?: number;
};
export type GaoPointChartReq = {
storeId?: string;
beginAt: number;
endAt: number;
};
export type GaoPointDailyStatView = {
dateValue?: number;
earnedPoints?: number;
consumedPoints?: number;
netChangePoints?: number;
};
export type GaoPointRedemptionProductRankView = {
productId?: string;
productName?: string;
redemptionCount?: number;
pointCost?: number;
};
export type GaoSalesOrderChartReq = {
storeId?: string;
customerId?: string;