修复泥点余额明细刷新

钱包明细每次展开并在实时余额变化时读取最新拆分。

图片画板生成扣费或退款后同步刷新总额与充值中心。

充值中心读取增加 latest-wins 门禁并阻止旧响应覆盖权威结果。

补充钱包刷新、画板入口和充值乱序回归测试。

同步更新泥点统一资产入口决策记录。
This commit is contained in:
2026-07-20 12:48:28 +08:00
parent 0c04bbbea3
commit f1f2332e61
7 changed files with 220 additions and 27 deletions
@@ -57,7 +57,7 @@ test('shows only permanent and daily free points in the shared wallet panel', as
expect(within(details).getByText('每日免费泥点')).toBeTruthy();
expect(within(details).getByText('27')).toBeTruthy();
expect(within(details).getByText('每天重置为 20 泥点')).toBeTruthy();
expect(onRequestDetails).not.toHaveBeenCalled();
expect(onRequestDetails).toHaveBeenCalledTimes(1);
await user.click(within(details).getByRole('button', { name: '使用详情' }));
expect(onOpenLedger).toHaveBeenCalledTimes(1);
@@ -85,6 +85,61 @@ test('keeps the chip on the live balance when cached details are stale', () => {
expect(screen.queryByRole('button', { name: '泥点 207' })).toBeNull();
});
test('refreshes cached balance details while open and after reopening', () => {
const onRequestDetails = vi.fn();
const { rerender } = render(
<PlatformMudPointWalletEntry
balance={207}
breakdown={breakdown}
onRequestDetails={onRequestDetails}
onRecharge={vi.fn()}
onOpenLedger={vi.fn()}
/>,
);
const balanceButton = screen.getByRole('button', { name: '泥点 207' });
fireEvent.mouseEnter(balanceButton);
expect(onRequestDetails).toHaveBeenCalledTimes(1);
rerender(
<PlatformMudPointWalletEntry
balance={195}
breakdown={breakdown}
onRequestDetails={onRequestDetails}
onRecharge={vi.fn()}
onOpenLedger={vi.fn()}
/>,
);
expect(screen.getByRole('dialog', { name: '泥点账户详情' })).toBeTruthy();
expect(onRequestDetails).toHaveBeenCalledTimes(2);
fireEvent.pointerDown(document.body);
fireEvent.mouseEnter(screen.getByRole('button', { name: '泥点 195' }));
expect(onRequestDetails).toHaveBeenCalledTimes(3);
});
test('requests missing details after the opening balance load completes', () => {
const onRequestDetails = vi.fn();
const props = {
balance: 20,
breakdown: null,
variant: 'mobile' as const,
onRequestDetails,
onRecharge: vi.fn(),
onOpenLedger: vi.fn(),
};
const { rerender } = render(
<PlatformMudPointWalletEntry {...props} isLoading />,
);
fireEvent.click(screen.getByRole('button', { name: '泥点 20' }));
expect(onRequestDetails).not.toHaveBeenCalled();
rerender(<PlatformMudPointWalletEntry {...props} isLoading={false} />);
expect(onRequestDetails).toHaveBeenCalledTimes(1);
});
test('keeps the desktop panel open while moving across the gap without click pinning', () => {
vi.useFakeTimers();
@@ -64,6 +64,7 @@ export function PlatformMudPointWalletEntry({
}: PlatformMudPointWalletEntryProps) {
const rootRef = useRef<HTMLDivElement | null>(null);
const isOpenRef = useRef(false);
const requestedDetailsKeyForOpenRef = useRef<string | null>(null);
const closeTimerRef = useRef<number | null>(null);
const [isOpen, setIsOpen] = useState(false);
const isCompact = variant === 'mobile';
@@ -74,6 +75,7 @@ export function PlatformMudPointWalletEntry({
: formatMudPointCount(displayedBalance, true);
const exactBalanceLabel =
displayedBalance === null ? '--' : formatMudPointCount(displayedBalance);
const detailsRequestKey = `balance:${balance ?? breakdown?.totalPoints ?? 'unknown'}`;
const cancelPendingClose = useCallback(() => {
if (closeTimerRef.current !== null) {
@@ -85,9 +87,21 @@ export function PlatformMudPointWalletEntry({
const closeDetails = useCallback(() => {
cancelPendingClose();
isOpenRef.current = false;
requestedDetailsKeyForOpenRef.current = null;
setIsOpen(false);
}, [cancelPendingClose]);
const requestDetailsIfNeeded = useCallback(() => {
if (
isLoading ||
requestedDetailsKeyForOpenRef.current === detailsRequestKey
) {
return;
}
requestedDetailsKeyForOpenRef.current = detailsRequestKey;
onRequestDetails();
}, [detailsRequestKey, isLoading, onRequestDetails]);
const requestAndOpen = useCallback(() => {
cancelPendingClose();
if (isOpenRef.current) {
@@ -95,13 +109,17 @@ export function PlatformMudPointWalletEntry({
}
isOpenRef.current = true;
setIsOpen(true);
if (!breakdown && !isLoading) {
onRequestDetails();
}
}, [breakdown, cancelPendingClose, isLoading, onRequestDetails]);
requestDetailsIfNeeded();
}, [cancelPendingClose, requestDetailsIfNeeded]);
useEffect(() => cancelPendingClose, [cancelPendingClose]);
useEffect(() => {
if (isOpen) {
requestDetailsIfNeeded();
}
}, [isOpen, requestDetailsIfNeeded]);
useEffect(() => {
if (!isOpen) {
return;
@@ -151,9 +169,7 @@ export function PlatformMudPointWalletEntry({
<div
ref={rootRef}
className={`platform-mud-point-wallet-entry relative shrink-0 ${className ?? ''}`}
onMouseEnter={isCompact ? undefined : requestAndOpen}
onMouseLeave={isCompact ? undefined : closeAfterPointerLeaves}
onFocusCapture={isCompact ? undefined : requestAndOpen}
onBlurCapture={closeAfterFocusLeaves}
>
<div
@@ -168,6 +184,8 @@ export function PlatformMudPointWalletEntry({
aria-expanded={isOpen}
aria-haspopup="dialog"
aria-busy={isLoading}
onMouseEnter={isCompact ? undefined : requestAndOpen}
onFocus={isCompact ? undefined : requestAndOpen}
onClick={
isCompact
? () => {