aura-web/src/pages/PointsMallPage/PointsMallPageLogic.ts

233 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { useEffect, useRef, useState } from 'react';
import { message } from 'antd';
import { PointsMallAPI, PointsMallCategory, PointsMallOverview, PointsMallProduct, PointsMallProductsResponse } from '../../api';
import { MOCK_OVERVIEW, MOCK_PRODUCTS } from './mocks';
import type { ExchangeFormValues, SortKey } from './types';
export function usePointsMallPageLogic() {
const [overviewLoading, setOverviewLoading] = useState(false);
const [productsLoading, setProductsLoading] = useState(false);
const [overview, setOverview] = useState<PointsMallOverview | null>(null);
const [categories, setCategories] = useState<PointsMallCategory[]>([]);
const [categoryId, setCategoryId] = useState<string>('all');
const [q, setQ] = useState('');
const [sort, setSort] = useState<SortKey>('popular');
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(12);
const [productsRes, setProductsRes] = useState<PointsMallProductsResponse | null>(null);
const [exchangeModalVisible, setExchangeModalVisible] = useState(false);
const [confirmModalVisible, setConfirmModalVisible] = useState(false);
const [selectedProduct, setSelectedProduct] = useState<PointsMallProduct | null>(null);
const [exchangeQuantity, setExchangeQuantity] = useState(1);
const [pendingOrderId, setPendingOrderId] = useState<string | null>(null);
const [pendingExpiresAt, setPendingExpiresAt] = useState<string | null>(null);
const [exchangeLoading, setExchangeLoading] = useState(false);
const exchangePrepareInFlightRef = useRef(false);
const loadOverview = async () => {
setOverviewLoading(true);
try {
const [meRes, categoriesRes, announcementsRes, bannersRes, promoEntriesRes] = await Promise.allSettled([
PointsMallAPI.me(),
PointsMallAPI.categories(),
PointsMallAPI.announcements(),
PointsMallAPI.banners(),
PointsMallAPI.promoEntries(),
]);
const rawMe = meRes.status === 'fulfilled' ? meRes.value : { points: 0, level: 'Lv.0' };
const me = {
...rawMe,
points: Number((rawMe as any)?.points ?? 0),
};
const loadedCats = categoriesRes.status === 'fulfilled' ? categoriesRes.value : MOCK_OVERVIEW.categories;
const cats = loadedCats?.length ? loadedCats : MOCK_OVERVIEW.categories;
const announcements = announcementsRes.status === 'fulfilled' ? announcementsRes.value : MOCK_OVERVIEW.announcements;
const banners = bannersRes.status === 'fulfilled' ? bannersRes.value : MOCK_OVERVIEW.banners;
const promoEntries = promoEntriesRes.status === 'fulfilled' ? promoEntriesRes.value : MOCK_OVERVIEW.promoEntries;
setOverview({ me, categories: cats, announcements, banners, promoEntries });
setCategories(cats);
if (!cats?.some((c) => c.id === categoryId) && cats?.[0]?.id) {
setCategoryId(cats[0].id);
}
if (meRes.status === 'rejected') {
message.error('获取Token信息失败请稍后重试');
}
} catch {
message.error('获取Token信息失败请稍后重试');
setOverview({ ...MOCK_OVERVIEW, me: { points: 0, level: 'Lv.0' } });
setCategories(MOCK_OVERVIEW.categories);
} finally {
setOverviewLoading(false);
}
};
const loadProducts = async () => {
setProductsLoading(true);
try {
const res = await PointsMallAPI.products({
categoryId: categoryId === 'all' ? undefined : categoryId,
q,
sort,
page,
pageSize,
});
if (Array.isArray(res.items) && res.items.length > 0) {
setProductsRes(res);
return;
}
const fallback = MOCK_PRODUCTS.filter((p) => (categoryId === 'all' ? true : p.categoryId === categoryId)).filter((p) =>
q ? (p.name + p.subtitle).toLowerCase().includes(q.toLowerCase()) : true
);
setProductsRes({
page,
pageSize,
total: fallback.length,
items: fallback.slice((page - 1) * pageSize, page * pageSize),
});
} catch {
const filtered = MOCK_PRODUCTS.filter((p) => (categoryId === 'all' ? true : p.categoryId === categoryId)).filter((p) =>
q ? (p.name + p.subtitle).toLowerCase().includes(q.toLowerCase()) : true
);
setProductsRes({
page,
pageSize,
total: filtered.length,
items: filtered.slice((page - 1) * pageSize, page * pageSize),
});
} finally {
setProductsLoading(false);
}
};
useEffect(() => {
loadOverview();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
loadProducts();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [categoryId, q, sort, page, pageSize]);
const banner = overview?.banners?.[0];
const promoEntries = overview?.promoEntries || [];
const products = productsRes?.items || [];
const total = productsRes?.total || 0;
const userPoints = overview?.me?.points || 0;
const totalSpentUSD = overview?.me?.totalSpentUSD;
const handleExchangeClick = async (product: PointsMallProduct) => {
setSelectedProduct(product);
setExchangeQuantity(1);
setConfirmModalVisible(true);
};
const handleConfirmExchange = async () => {
if (!selectedProduct) return;
if (userPoints < selectedProduct.pointsPrice * exchangeQuantity) return;
if (exchangePrepareInFlightRef.current) return;
exchangePrepareInFlightRef.current = true;
setExchangeLoading(true);
try {
const res = await PointsMallAPI.exchangePrepare(selectedProduct.id, exchangeQuantity);
setPendingOrderId(res.orderId);
setPendingExpiresAt(res.expiresAt || new Date(Date.now() + 30 * 60 * 1000).toISOString());
setConfirmModalVisible(false);
setExchangeModalVisible(true);
const remainingPoints = res.remainingPoints;
if (typeof remainingPoints === 'number') {
setOverview((prev) => {
if (!prev) return prev;
return { ...prev, me: { ...prev.me, points: remainingPoints } };
});
}
message.success('已冻结Token并预扣库存请继续填写收件信息');
} catch (e: any) {
const msg = e?.response?.data?.message || e?.response?.data?.error || e?.message || '兑换失败,请稍后重试';
message.error(msg);
} finally {
setExchangeLoading(false);
exchangePrepareInFlightRef.current = false;
}
};
const handleExchangeSubmit = async (values: ExchangeFormValues) => {
if (!selectedProduct || !pendingOrderId) return;
setExchangeLoading(true);
try {
const res = await PointsMallAPI.exchangeSubmitShipping(pendingOrderId, values);
setExchangeModalVisible(false);
setConfirmModalVisible(false);
setPendingOrderId(null);
setPendingExpiresAt(null);
setSelectedProduct(null);
setExchangeQuantity(1);
const remainingPoints = res.remainingPoints;
if (typeof remainingPoints === 'number') {
setOverview((prev) => {
if (!prev) return prev;
return { ...prev, me: { ...prev.me, points: remainingPoints } };
});
} else {
loadOverview();
}
loadProducts();
message.success('兑换成功');
} catch (e: any) {
const msg = e?.response?.data?.message || e?.response?.data?.error || e?.message || '提交收件信息失败,请稍后重试';
message.error(msg);
} finally {
setExchangeLoading(false);
}
};
return {
overview,
overviewLoading,
categories,
categoryId,
q,
sort,
page,
pageSize,
productsLoading,
products,
total,
userPoints,
totalSpentUSD,
banner,
promoEntries,
exchangeModalVisible,
confirmModalVisible,
selectedProduct,
exchangeQuantity,
pendingOrderId,
pendingExpiresAt,
exchangeLoading,
setCategoryId,
setQ,
setSort,
setPage,
setPageSize,
handleExchangeClick,
handleConfirmExchange,
setExchangeModalVisible,
setConfirmModalVisible,
setPendingOrderId,
setPendingExpiresAt,
setSelectedProduct,
setExchangeQuantity,
setOverview,
handleExchangeSubmit,
};
}
export type PointsMallPageLogicOutput = ReturnType<typeof usePointsMallPageLogic>;