233 lines
8.1 KiB
TypeScript
233 lines
8.1 KiB
TypeScript
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>;
|