feat(customer): implement customer analysis page and drawer flow
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import type { OptionItem } from '../../types';
|
||||
|
||||
import type {
|
||||
CustomerAnalysisOverviewDto,
|
||||
CustomerAnalysisPeriodFilter,
|
||||
CustomerAnalysisSegmentCode,
|
||||
} from '#/api/customer';
|
||||
|
||||
/** 客户分析查看权限。 */
|
||||
export const CUSTOMER_ANALYSIS_VIEW_PERMISSION =
|
||||
'tenant:customer:analysis:view';
|
||||
|
||||
/** 统计周期选项。 */
|
||||
export const ANALYSIS_PERIOD_OPTIONS: OptionItem[] = [
|
||||
{ label: '近7天', value: '7d' },
|
||||
{ label: '近30天', value: '30d' },
|
||||
{ label: '近90天', value: '90d' },
|
||||
{ label: '近1年', value: '365d' },
|
||||
];
|
||||
|
||||
/** 默认统计周期。 */
|
||||
export const DEFAULT_PERIOD: CustomerAnalysisPeriodFilter = '30d';
|
||||
|
||||
/** 默认分群。 */
|
||||
export const DEFAULT_SEGMENT_CODE: CustomerAnalysisSegmentCode = 'all';
|
||||
|
||||
/** 默认总览数据。 */
|
||||
export const EMPTY_OVERVIEW: CustomerAnalysisOverviewDto = {
|
||||
periodCode: DEFAULT_PERIOD,
|
||||
periodDays: 30,
|
||||
totalCustomers: 0,
|
||||
newCustomers: 0,
|
||||
growthRatePercent: 0,
|
||||
newCustomersDailyAverage: 0,
|
||||
activeCustomers: 0,
|
||||
activeRatePercent: 0,
|
||||
averageLifetimeValue: 0,
|
||||
growthTrend: [],
|
||||
composition: [],
|
||||
amountDistribution: [],
|
||||
rfmRows: [],
|
||||
topCustomers: [],
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import type { CustomerAnalysisOverviewDto } from '#/api/customer';
|
||||
import type { StoreListItemDto } from '#/api/store';
|
||||
|
||||
import { getCustomerAnalysisOverviewApi } from '#/api/customer';
|
||||
import { getStoreListApi } from '#/api/store';
|
||||
|
||||
import { EMPTY_OVERVIEW } from './constants';
|
||||
|
||||
interface DataActionOptions {
|
||||
isOverviewLoading: Ref<boolean>;
|
||||
isStoreLoading: Ref<boolean>;
|
||||
overview: Ref<CustomerAnalysisOverviewDto>;
|
||||
period: Ref<'7d' | '30d' | '90d' | '365d'>;
|
||||
selectedStoreId: Ref<string>;
|
||||
stores: Ref<StoreListItemDto[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件职责:客户分析页的数据加载动作。
|
||||
*/
|
||||
export function createDataActions(options: DataActionOptions) {
|
||||
function resolvePeriodDays(period: '7d' | '30d' | '90d' | '365d'): number {
|
||||
switch (period) {
|
||||
case '7d': {
|
||||
return 7;
|
||||
}
|
||||
case '90d': {
|
||||
return 90;
|
||||
}
|
||||
case '365d': {
|
||||
return 365;
|
||||
}
|
||||
default: {
|
||||
return 30;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetOverview() {
|
||||
options.overview.value = {
|
||||
...EMPTY_OVERVIEW,
|
||||
periodCode: options.period.value,
|
||||
periodDays: resolvePeriodDays(options.period.value),
|
||||
};
|
||||
}
|
||||
|
||||
async function loadStores() {
|
||||
options.isStoreLoading.value = true;
|
||||
try {
|
||||
const result = await getStoreListApi({ page: 1, pageSize: 200 });
|
||||
options.stores.value = result.items;
|
||||
|
||||
if (result.items.length === 0) {
|
||||
options.selectedStoreId.value = '';
|
||||
resetOverview();
|
||||
return;
|
||||
}
|
||||
|
||||
const matched = result.items.some(
|
||||
(item) => item.id === options.selectedStoreId.value,
|
||||
);
|
||||
if (!matched) {
|
||||
options.selectedStoreId.value = result.items[0]?.id ?? '';
|
||||
}
|
||||
} finally {
|
||||
options.isStoreLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOverview() {
|
||||
if (!options.selectedStoreId.value) {
|
||||
resetOverview();
|
||||
return;
|
||||
}
|
||||
|
||||
options.isOverviewLoading.value = true;
|
||||
try {
|
||||
options.overview.value = await getCustomerAnalysisOverviewApi({
|
||||
storeId: options.selectedStoreId.value,
|
||||
period: options.period.value,
|
||||
});
|
||||
} finally {
|
||||
options.isOverviewLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loadOverview,
|
||||
loadStores,
|
||||
resetOverview,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import type { CustomerDetailDto, CustomerProfileDto } from '#/api/customer';
|
||||
|
||||
import {
|
||||
getCustomerAnalysisDetailApi,
|
||||
getCustomerAnalysisProfileApi,
|
||||
} from '#/api/customer';
|
||||
|
||||
interface DrawerActionOptions {
|
||||
detail: Ref<CustomerDetailDto | null>;
|
||||
isDetailDrawerOpen: Ref<boolean>;
|
||||
isDetailLoading: Ref<boolean>;
|
||||
isProfileDrawerOpen: Ref<boolean>;
|
||||
isProfileLoading: Ref<boolean>;
|
||||
profile: Ref<CustomerProfileDto | null>;
|
||||
selectedStoreId: Ref<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件职责:客户分析页客户详情与画像抽屉动作。
|
||||
*/
|
||||
export function createDrawerActions(options: DrawerActionOptions) {
|
||||
function setDetailDrawerOpen(value: boolean) {
|
||||
options.isDetailDrawerOpen.value = value;
|
||||
if (!value) {
|
||||
options.detail.value = null;
|
||||
options.isProfileDrawerOpen.value = false;
|
||||
options.profile.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function setProfileDrawerOpen(value: boolean) {
|
||||
options.isProfileDrawerOpen.value = value;
|
||||
if (!value) {
|
||||
options.profile.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function openDetail(customerKey: string) {
|
||||
if (!options.selectedStoreId.value || !customerKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
options.isDetailDrawerOpen.value = true;
|
||||
options.detail.value = null;
|
||||
options.isProfileDrawerOpen.value = false;
|
||||
options.profile.value = null;
|
||||
options.isDetailLoading.value = true;
|
||||
try {
|
||||
options.detail.value = await getCustomerAnalysisDetailApi({
|
||||
storeId: options.selectedStoreId.value,
|
||||
customerKey,
|
||||
});
|
||||
} finally {
|
||||
options.isDetailLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function openProfile(customerKey: string) {
|
||||
if (!options.selectedStoreId.value || !customerKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
options.isProfileDrawerOpen.value = true;
|
||||
options.profile.value = null;
|
||||
options.isProfileLoading.value = true;
|
||||
try {
|
||||
options.profile.value = await getCustomerAnalysisProfileApi({
|
||||
storeId: options.selectedStoreId.value,
|
||||
customerKey,
|
||||
});
|
||||
} finally {
|
||||
options.isProfileLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
openDetail,
|
||||
openProfile,
|
||||
setDetailDrawerOpen,
|
||||
setProfileDrawerOpen,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { exportCustomerAnalysisCsvApi } from '#/api/customer';
|
||||
|
||||
import { downloadBase64File } from './helpers';
|
||||
|
||||
interface ExportActionOptions {
|
||||
canExport: Ref<boolean>;
|
||||
isExporting: Ref<boolean>;
|
||||
period: Ref<'7d' | '30d' | '90d' | '365d'>;
|
||||
selectedStoreId: Ref<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件职责:客户分析报表导出动作。
|
||||
*/
|
||||
export function createExportActions(options: ExportActionOptions) {
|
||||
async function handleExport() {
|
||||
if (!options.selectedStoreId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!options.canExport.value) {
|
||||
message.warning('暂无导出权限');
|
||||
return;
|
||||
}
|
||||
|
||||
options.isExporting.value = true;
|
||||
try {
|
||||
const result = await exportCustomerAnalysisCsvApi({
|
||||
storeId: options.selectedStoreId.value,
|
||||
period: options.period.value,
|
||||
});
|
||||
downloadBase64File(result.fileName, result.fileContentBase64);
|
||||
message.success('客户分析报表导出成功');
|
||||
} finally {
|
||||
options.isExporting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handleExport,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
export function formatCurrency(value: number) {
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: 'CNY',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(Number.isFinite(value) ? value : 0);
|
||||
}
|
||||
|
||||
export function formatCurrencyWithFraction(value: number) {
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: 'CNY',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(Number.isFinite(value) ? value : 0);
|
||||
}
|
||||
|
||||
export function formatInteger(value: number) {
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
maximumFractionDigits: 0,
|
||||
}).format(Number.isFinite(value) ? value : 0);
|
||||
}
|
||||
|
||||
export function formatPercent(value: number) {
|
||||
if (!Number.isFinite(value)) {
|
||||
return '0%';
|
||||
}
|
||||
return `${value.toFixed(1).replace(/\.0$/, '')}%`;
|
||||
}
|
||||
|
||||
export function formatSignedPercent(value: number) {
|
||||
const normalized = Number.isFinite(value) ? value : 0;
|
||||
const sign = normalized > 0 ? '+' : '';
|
||||
return `${sign}${formatPercent(normalized)}`;
|
||||
}
|
||||
|
||||
export function resolveTagColor(tone: string) {
|
||||
if (tone === 'orange') return 'orange';
|
||||
if (tone === 'green') return 'green';
|
||||
if (tone === 'gray') return 'default';
|
||||
if (tone === 'red') return 'red';
|
||||
return 'blue';
|
||||
}
|
||||
|
||||
export function resolveRfmCellToneClass(tone: string) {
|
||||
if (tone === 'hot') return 'hot';
|
||||
if (tone === 'warm') return 'warm';
|
||||
if (tone === 'cool') return 'cool';
|
||||
return 'cold';
|
||||
}
|
||||
|
||||
export function resolveCompositionToneColor(tone: string) {
|
||||
if (tone === 'green') return '#52c41a';
|
||||
if (tone === 'orange') return '#fa8c16';
|
||||
if (tone === 'gray') return '#e5e7eb';
|
||||
return '#1677ff';
|
||||
}
|
||||
|
||||
export function resolveDistributionColor(index: number) {
|
||||
const palette = ['#7fb4ff', '#4a95ff', '#1677ff', '#1061d6', '#0a4eaf'];
|
||||
return palette[Math.max(0, Math.min(index, palette.length - 1))] ?? '#1677ff';
|
||||
}
|
||||
|
||||
export function resolveAvatarText(name: string) {
|
||||
const normalized = String(name || '').trim();
|
||||
return normalized ? normalized.slice(0, 1) : '客';
|
||||
}
|
||||
|
||||
function decodeBase64ToBlob(base64: string) {
|
||||
const binary = atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let index = 0; index < binary.length; index += 1) {
|
||||
bytes[index] = binary.codePointAt(index) ?? 0;
|
||||
}
|
||||
return new Blob([bytes], { type: 'text/csv;charset=utf-8;' });
|
||||
}
|
||||
|
||||
export function downloadBase64File(
|
||||
fileName: string,
|
||||
fileContentBase64: string,
|
||||
) {
|
||||
const blob = decodeBase64ToBlob(fileContentBase64);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = fileName;
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import type { CustomerMemberDetailDto } from '#/api/customer';
|
||||
|
||||
import { getCustomerMemberDetailApi } from '#/api/customer';
|
||||
|
||||
interface MemberActionOptions {
|
||||
detail: Ref<CustomerMemberDetailDto | null>;
|
||||
isMemberDrawerOpen: Ref<boolean>;
|
||||
isMemberLoading: Ref<boolean>;
|
||||
selectedStoreId: Ref<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件职责:客户分析页会员详情抽屉动作。
|
||||
*/
|
||||
export function createMemberActions(options: MemberActionOptions) {
|
||||
function setMemberDrawerOpen(value: boolean) {
|
||||
options.isMemberDrawerOpen.value = value;
|
||||
if (!value) {
|
||||
options.detail.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function openMember(customerKey: string) {
|
||||
if (!options.selectedStoreId.value || !customerKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
options.isMemberDrawerOpen.value = true;
|
||||
options.detail.value = null;
|
||||
options.isMemberLoading.value = true;
|
||||
try {
|
||||
options.detail.value = await getCustomerMemberDetailApi({
|
||||
storeId: options.selectedStoreId.value,
|
||||
customerKey,
|
||||
});
|
||||
} finally {
|
||||
options.isMemberLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
openMember,
|
||||
setMemberDrawerOpen,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import type {
|
||||
CustomerAnalysisSegmentCode,
|
||||
CustomerAnalysisSegmentListResultDto,
|
||||
} from '#/api/customer';
|
||||
|
||||
import { getCustomerAnalysisSegmentListApi } from '#/api/customer';
|
||||
|
||||
interface SegmentPagination {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface SegmentActionOptions {
|
||||
currentSegmentCode: Ref<CustomerAnalysisSegmentCode>;
|
||||
isSegmentDrawerOpen: Ref<boolean>;
|
||||
isSegmentLoading: Ref<boolean>;
|
||||
keyword: Ref<string>;
|
||||
pagination: SegmentPagination;
|
||||
period: Ref<'7d' | '30d' | '90d' | '365d'>;
|
||||
result: Ref<CustomerAnalysisSegmentListResultDto | null>;
|
||||
selectedStoreId: Ref<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件职责:客户分析页客群明细抽屉动作。
|
||||
*/
|
||||
export function createSegmentActions(options: SegmentActionOptions) {
|
||||
function setSegmentDrawerOpen(value: boolean) {
|
||||
options.isSegmentDrawerOpen.value = value;
|
||||
if (!value) {
|
||||
options.result.value = null;
|
||||
options.keyword.value = '';
|
||||
options.pagination.page = 1;
|
||||
options.pagination.total = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function setSegmentKeyword(value: string) {
|
||||
options.keyword.value = value;
|
||||
}
|
||||
|
||||
async function loadSegmentData() {
|
||||
if (!options.selectedStoreId.value) {
|
||||
options.result.value = null;
|
||||
options.pagination.total = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
options.isSegmentLoading.value = true;
|
||||
try {
|
||||
const result = await getCustomerAnalysisSegmentListApi({
|
||||
storeId: options.selectedStoreId.value,
|
||||
period: options.period.value,
|
||||
segmentCode: options.currentSegmentCode.value,
|
||||
keyword: options.keyword.value.trim() || undefined,
|
||||
page: options.pagination.page,
|
||||
pageSize: options.pagination.pageSize,
|
||||
});
|
||||
|
||||
options.result.value = result;
|
||||
options.pagination.page = result.page;
|
||||
options.pagination.pageSize = result.pageSize;
|
||||
options.pagination.total = result.totalCount;
|
||||
} finally {
|
||||
options.isSegmentLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function openSegment(segmentCode: CustomerAnalysisSegmentCode) {
|
||||
options.currentSegmentCode.value = segmentCode;
|
||||
options.keyword.value = '';
|
||||
options.pagination.page = 1;
|
||||
options.isSegmentDrawerOpen.value = true;
|
||||
await loadSegmentData();
|
||||
}
|
||||
|
||||
async function handleSegmentSearch() {
|
||||
options.pagination.page = 1;
|
||||
await loadSegmentData();
|
||||
}
|
||||
|
||||
async function handleSegmentPageChange(page: number, pageSize: number) {
|
||||
options.pagination.page = page;
|
||||
options.pagination.pageSize = pageSize;
|
||||
await loadSegmentData();
|
||||
}
|
||||
|
||||
return {
|
||||
handleSegmentPageChange,
|
||||
handleSegmentSearch,
|
||||
loadSegmentData,
|
||||
openSegment,
|
||||
setSegmentDrawerOpen,
|
||||
setSegmentKeyword,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
import type {
|
||||
CustomerAnalysisOverviewDto,
|
||||
CustomerAnalysisPeriodFilter,
|
||||
CustomerAnalysisSegmentCode,
|
||||
CustomerAnalysisSegmentListResultDto,
|
||||
CustomerDetailDto,
|
||||
CustomerMemberDetailDto,
|
||||
CustomerProfileDto,
|
||||
} from '#/api/customer';
|
||||
import type { StoreListItemDto } from '#/api/store';
|
||||
|
||||
import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import { createNavigationActions } from '#/views/customer/list/composables/customer-list-page/navigation-actions';
|
||||
|
||||
import {
|
||||
CUSTOMER_ANALYSIS_VIEW_PERMISSION,
|
||||
DEFAULT_PERIOD,
|
||||
DEFAULT_SEGMENT_CODE,
|
||||
EMPTY_OVERVIEW,
|
||||
} from './customer-analysis-page/constants';
|
||||
import { createDataActions } from './customer-analysis-page/data-actions';
|
||||
import { createDrawerActions } from './customer-analysis-page/drawer-actions';
|
||||
import { createExportActions } from './customer-analysis-page/export-actions';
|
||||
import { createMemberActions } from './customer-analysis-page/member-actions';
|
||||
import { createSegmentActions } from './customer-analysis-page/segment-actions';
|
||||
|
||||
export function useCustomerAnalysisPage() {
|
||||
const accessStore = useAccessStore();
|
||||
const router = useRouter();
|
||||
|
||||
const stores = ref<StoreListItemDto[]>([]);
|
||||
const selectedStoreId = ref('');
|
||||
const isStoreLoading = ref(false);
|
||||
|
||||
const period = ref<CustomerAnalysisPeriodFilter>(DEFAULT_PERIOD);
|
||||
const overview = ref<CustomerAnalysisOverviewDto>({ ...EMPTY_OVERVIEW });
|
||||
const isOverviewLoading = ref(false);
|
||||
|
||||
const segmentResult = ref<CustomerAnalysisSegmentListResultDto | null>(null);
|
||||
const isSegmentDrawerOpen = ref(false);
|
||||
const isSegmentLoading = ref(false);
|
||||
const segmentKeyword = ref('');
|
||||
const currentSegmentCode =
|
||||
ref<CustomerAnalysisSegmentCode>(DEFAULT_SEGMENT_CODE);
|
||||
const segmentPagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const detail = ref<CustomerDetailDto | null>(null);
|
||||
const isDetailDrawerOpen = ref(false);
|
||||
const isDetailLoading = ref(false);
|
||||
|
||||
const profile = ref<CustomerProfileDto | null>(null);
|
||||
const isProfileDrawerOpen = ref(false);
|
||||
const isProfileLoading = ref(false);
|
||||
|
||||
const memberDetail = ref<CustomerMemberDetailDto | null>(null);
|
||||
const isMemberDrawerOpen = ref(false);
|
||||
const isMemberLoading = ref(false);
|
||||
|
||||
const isExporting = ref(false);
|
||||
|
||||
const storeOptions = computed(() =>
|
||||
stores.value.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
})),
|
||||
);
|
||||
|
||||
const accessCodeSet = computed(
|
||||
() => new Set((accessStore.accessCodes ?? []).map(String)),
|
||||
);
|
||||
const canExport = computed(() =>
|
||||
accessCodeSet.value.has(CUSTOMER_ANALYSIS_VIEW_PERMISSION),
|
||||
);
|
||||
|
||||
const { loadStores, loadOverview, resetOverview } = createDataActions({
|
||||
stores,
|
||||
selectedStoreId,
|
||||
period,
|
||||
overview,
|
||||
isStoreLoading,
|
||||
isOverviewLoading,
|
||||
});
|
||||
|
||||
const {
|
||||
openSegment,
|
||||
loadSegmentData,
|
||||
setSegmentDrawerOpen,
|
||||
setSegmentKeyword,
|
||||
handleSegmentSearch,
|
||||
handleSegmentPageChange,
|
||||
} = createSegmentActions({
|
||||
selectedStoreId,
|
||||
period,
|
||||
currentSegmentCode,
|
||||
keyword: segmentKeyword,
|
||||
result: segmentResult,
|
||||
isSegmentDrawerOpen,
|
||||
isSegmentLoading,
|
||||
pagination: segmentPagination,
|
||||
});
|
||||
|
||||
const { openMember, setMemberDrawerOpen } = createMemberActions({
|
||||
selectedStoreId,
|
||||
detail: memberDetail,
|
||||
isMemberDrawerOpen,
|
||||
isMemberLoading,
|
||||
});
|
||||
|
||||
const { openDetail, openProfile, setDetailDrawerOpen, setProfileDrawerOpen } =
|
||||
createDrawerActions({
|
||||
selectedStoreId,
|
||||
detail,
|
||||
isDetailDrawerOpen,
|
||||
isDetailLoading,
|
||||
profile,
|
||||
isProfileDrawerOpen,
|
||||
isProfileLoading,
|
||||
});
|
||||
|
||||
const { openProfilePage } = createNavigationActions({
|
||||
selectedStoreId,
|
||||
router,
|
||||
});
|
||||
|
||||
const { handleExport } = createExportActions({
|
||||
selectedStoreId,
|
||||
period,
|
||||
isExporting,
|
||||
canExport,
|
||||
});
|
||||
|
||||
function setSelectedStoreId(value: string) {
|
||||
selectedStoreId.value = value;
|
||||
}
|
||||
|
||||
function setPeriod(value: string) {
|
||||
const normalized = (value ||
|
||||
DEFAULT_PERIOD) as CustomerAnalysisPeriodFilter;
|
||||
period.value = normalized;
|
||||
}
|
||||
|
||||
async function openSegmentByCode(segmentCode: CustomerAnalysisSegmentCode) {
|
||||
await openSegment(segmentCode);
|
||||
}
|
||||
|
||||
async function openTopCustomerDetail(customerKey: string) {
|
||||
await openDetail(customerKey);
|
||||
}
|
||||
|
||||
async function openMemberFromDetail(customerKey: string) {
|
||||
await openMember(customerKey);
|
||||
}
|
||||
|
||||
watch(selectedStoreId, async (storeId) => {
|
||||
if (!storeId) {
|
||||
resetOverview();
|
||||
segmentResult.value = null;
|
||||
segmentPagination.total = 0;
|
||||
setDetailDrawerOpen(false);
|
||||
setProfileDrawerOpen(false);
|
||||
setMemberDrawerOpen(false);
|
||||
isSegmentDrawerOpen.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
await loadOverview();
|
||||
if (isSegmentDrawerOpen.value) {
|
||||
await loadSegmentData();
|
||||
}
|
||||
});
|
||||
|
||||
watch(period, async () => {
|
||||
if (!selectedStoreId.value) {
|
||||
resetOverview();
|
||||
return;
|
||||
}
|
||||
|
||||
await loadOverview();
|
||||
if (isSegmentDrawerOpen.value) {
|
||||
await loadSegmentData();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
void loadStores();
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
if (stores.value.length === 0 || !selectedStoreId.value) {
|
||||
void loadStores();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
canExport,
|
||||
currentSegmentCode,
|
||||
detail,
|
||||
handleExport,
|
||||
handleSegmentPageChange,
|
||||
handleSegmentSearch,
|
||||
isDetailDrawerOpen,
|
||||
isDetailLoading,
|
||||
isExporting,
|
||||
isMemberDrawerOpen,
|
||||
isMemberLoading,
|
||||
isOverviewLoading,
|
||||
isProfileDrawerOpen,
|
||||
isProfileLoading,
|
||||
isSegmentDrawerOpen,
|
||||
isSegmentLoading,
|
||||
isStoreLoading,
|
||||
memberDetail,
|
||||
openDetail,
|
||||
openMember,
|
||||
openMemberFromDetail,
|
||||
openProfile,
|
||||
openProfilePage,
|
||||
openSegmentByCode,
|
||||
openTopCustomerDetail,
|
||||
overview,
|
||||
period,
|
||||
profile,
|
||||
segmentKeyword,
|
||||
segmentPagination,
|
||||
segmentResult,
|
||||
selectedStoreId,
|
||||
setDetailDrawerOpen,
|
||||
setMemberDrawerOpen,
|
||||
setPeriod,
|
||||
setProfileDrawerOpen,
|
||||
setSegmentDrawerOpen,
|
||||
setSegmentKeyword,
|
||||
setSelectedStoreId,
|
||||
storeOptions,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user