@@ -1,6 +1,7 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { request } from '../lib/api';
|
||||
import { toastError, toastSuccess } from '../lib/toast';
|
||||
import { uploadRedeemPendingPhoto } from '../lib/upload';
|
||||
import { isWechatEnv, weixinSdk } from '../lib/weixin';
|
||||
import type { RedeemPendingSubmitResult } from '@dukang/shared-types';
|
||||
@@ -18,25 +19,22 @@ export default function WeakNetFallbackPanel({ redeemToken, failCount }: Props)
|
||||
const [previewUrl, setPreviewUrl] = useState('');
|
||||
const [photoResourceId, setPhotoResourceId] = useState('');
|
||||
const [result, setResult] = useState<RedeemPendingSubmitResult | null>(null);
|
||||
const [msg, setMsg] = useState('');
|
||||
const [showAlbumFallback, setShowAlbumFallback] = useState(false);
|
||||
|
||||
async function handleFile(file: File) {
|
||||
setUploading(true);
|
||||
setMsg('');
|
||||
try {
|
||||
const registered = await uploadRedeemPendingPhoto(file);
|
||||
setPhotoResourceId(registered.id);
|
||||
setPreviewUrl(registered.url);
|
||||
} catch (e) {
|
||||
setMsg(e instanceof Error ? e.message : '上传失败');
|
||||
toastError(e instanceof Error ? e.message : '上传失败');
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function pickPhoto() {
|
||||
setMsg('');
|
||||
if (isWechatEnv()) {
|
||||
try {
|
||||
setUploading(true);
|
||||
@@ -51,7 +49,7 @@ export default function WeakNetFallbackPanel({ redeemToken, failCount }: Props)
|
||||
} catch (e) {
|
||||
const text = e instanceof Error ? e.message : '选图失败';
|
||||
if (!/cancel/i.test(text)) {
|
||||
setMsg(`${text},可改从系统相册选择`);
|
||||
toastError(`${text},可改从系统相册选择`);
|
||||
setShowAlbumFallback(true);
|
||||
inputRef.current?.click();
|
||||
}
|
||||
@@ -65,11 +63,10 @@ export default function WeakNetFallbackPanel({ redeemToken, failCount }: Props)
|
||||
|
||||
async function submitPending() {
|
||||
if (!photoResourceId) {
|
||||
setMsg('请先拍摄或上传核销码照片');
|
||||
toastError('请先拍摄或上传核销码照片');
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
setMsg('');
|
||||
try {
|
||||
const res = await request<RedeemPendingSubmitResult>('SHOP_H5', '/shop/redeem/pending', {
|
||||
method: 'POST',
|
||||
@@ -81,7 +78,7 @@ export default function WeakNetFallbackPanel({ redeemToken, failCount }: Props)
|
||||
});
|
||||
setResult(res);
|
||||
} catch (e) {
|
||||
setMsg(e instanceof Error ? e.message : '提交失败');
|
||||
toastError(e instanceof Error ? e.message : '提交失败');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
@@ -89,8 +86,8 @@ export default function WeakNetFallbackPanel({ redeemToken, failCount }: Props)
|
||||
|
||||
function copyText(text: string) {
|
||||
void navigator.clipboard?.writeText(text).then(
|
||||
() => setMsg('已复制'),
|
||||
() => setMsg('复制失败,请手动长按复制'),
|
||||
() => toastSuccess('已复制'),
|
||||
() => toastError('复制失败,请手动长按复制'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -169,8 +166,6 @@ export default function WeakNetFallbackPanel({ redeemToken, failCount }: Props)
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{msg && <p className="shop-redeem-error" style={{ marginTop: 12 }}>{msg}</p>}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useRef, useState, type ReactNode } from 'react';
|
||||
import { registerShopToastListener, type ShopToastVariant } from '../lib/toast';
|
||||
|
||||
type ShopToastContextValue = {
|
||||
showToast: (message: string, variant?: ShopToastVariant) => void;
|
||||
};
|
||||
|
||||
const ShopToastContext = createContext<ShopToastContextValue | null>(null);
|
||||
|
||||
export function ShopToastProvider({ children }: { children: ReactNode }) {
|
||||
const [toast, setToast] = useState('');
|
||||
const [variant, setVariant] = useState<ShopToastVariant>('error');
|
||||
const timerRef = useRef<number | null>(null);
|
||||
|
||||
const showToast = useCallback((message: string, nextVariant: ShopToastVariant = 'error') => {
|
||||
if (timerRef.current) window.clearTimeout(timerRef.current);
|
||||
setVariant(nextVariant);
|
||||
setToast(message);
|
||||
timerRef.current = window.setTimeout(() => {
|
||||
setToast('');
|
||||
timerRef.current = null;
|
||||
}, nextVariant === 'error' ? 2800 : 2000);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
registerShopToastListener(showToast);
|
||||
return () => {
|
||||
registerShopToastListener(null);
|
||||
if (timerRef.current) window.clearTimeout(timerRef.current);
|
||||
};
|
||||
}, [showToast]);
|
||||
|
||||
return (
|
||||
<ShopToastContext.Provider value={{ showToast }}>
|
||||
{children}
|
||||
{toast ? (
|
||||
<div
|
||||
className={`shop-float-toast${variant === 'error' ? ' shop-float-toast--error' : ''}`}
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
>
|
||||
{toast}
|
||||
</div>
|
||||
) : null}
|
||||
</ShopToastContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useShopToast(): ShopToastContextValue {
|
||||
const ctx = useContext(ShopToastContext);
|
||||
if (!ctx) throw new Error('useShopToast 必须在 ShopToastProvider 内使用');
|
||||
return ctx;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/** 把接口里的金额(number / 数字字符串 / Prisma Decimal 残影)转成有限数字 */
|
||||
export function toMoneyNumber(value: unknown): number {
|
||||
if (typeof value === 'number') return Number.isFinite(value) ? value : 0;
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
}
|
||||
if (value && typeof value === 'object') {
|
||||
const o = value as { toNumber?: () => number; toString?: () => string; d?: unknown };
|
||||
if (typeof o.toNumber === 'function') {
|
||||
const n = Number(o.toNumber());
|
||||
if (Number.isFinite(n)) return n;
|
||||
}
|
||||
if (typeof o.toString === 'function' && o.toString !== Object.prototype.toString) {
|
||||
const n = Number(o.toString());
|
||||
if (Number.isFinite(n)) return n;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function formatMoney(n: number) {
|
||||
return toMoneyNumber(n).toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export type ShopToastVariant = 'success' | 'error';
|
||||
|
||||
type ShopToastListener = (message: string, variant: ShopToastVariant) => void;
|
||||
|
||||
let listener: ShopToastListener | null = null;
|
||||
|
||||
export function registerShopToastListener(fn: ShopToastListener | null) {
|
||||
listener = fn;
|
||||
}
|
||||
|
||||
export function showShopToast(message: string, variant: ShopToastVariant = 'error') {
|
||||
const text = message.trim();
|
||||
if (!text || !listener) return;
|
||||
listener(text, variant);
|
||||
}
|
||||
|
||||
export function toastError(message: string) {
|
||||
showShopToast(message, 'error');
|
||||
}
|
||||
|
||||
export function toastSuccess(message: string) {
|
||||
showShopToast(message, 'success');
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { getRouterBasename } from '@dukang/weixin-sdk';
|
||||
import { StoreSessionProvider } from './contexts/StoreSessionContext';
|
||||
import { ShopToastProvider } from './contexts/ShopToastContext';
|
||||
import { installClientErrorReporting } from '@dukang/client-logging';
|
||||
import App from './App';
|
||||
import { apiBase } from './lib/api';
|
||||
@@ -19,7 +20,9 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter basename={getRouterBasename()}>
|
||||
<StoreSessionProvider>
|
||||
<App />
|
||||
<ShopToastProvider>
|
||||
<App />
|
||||
</ShopToastProvider>
|
||||
</StoreSessionProvider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { isIosDevice, isScanPermissionWarmupError } from '@dukang/weixin-sdk';
|
||||
import { useStoreSession } from '../contexts/StoreSessionContext';
|
||||
|
||||
import { request } from '../lib/api';
|
||||
import { formatMoney, toMoneyNumber } from '../lib/money';
|
||||
|
||||
import { parseRedeemTokenFromScan } from '../lib/redeem-scan';
|
||||
|
||||
@@ -46,14 +47,6 @@ import { trackStore } from '../lib/analytics';
|
||||
|
||||
|
||||
|
||||
function formatMoney(n: number) {
|
||||
|
||||
return n.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function formatScanError(e: unknown, opts?: { afterAuth?: boolean }): string {
|
||||
|
||||
const msg = e instanceof Error ? e.message : '扫码失败,请重试';
|
||||
@@ -476,7 +469,7 @@ export default function HomePage() {
|
||||
|
||||
<span style={{ fontSize: 18 }}>¥</span>
|
||||
|
||||
{formatMoney(Number(dash?.todayAmount || 0))}
|
||||
{formatMoney(toMoneyNumber(dash?.todayAmount))}
|
||||
|
||||
</p>
|
||||
|
||||
@@ -596,7 +589,7 @@ export default function HomePage() {
|
||||
|
||||
</div>
|
||||
|
||||
<p className="shop-home-record-amount">¥{formatMoney(Number(r.amount))}</p>
|
||||
<p className="shop-home-record-amount">¥{formatMoney(toMoneyNumber(r.amount))}</p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import type { RedeemPhoneDirectPrepareResult } from '@dukang/shared-types';
|
||||
import { request } from '../lib/api';
|
||||
import { toastError, toastSuccess } from '../lib/toast';
|
||||
import { useStorePageView } from '../lib/usePageView';
|
||||
|
||||
function formatAmount(n: number) {
|
||||
@@ -17,7 +18,6 @@ export default function PhoneRedeemPage() {
|
||||
const [prepared, setPrepared] = useState<RedeemPhoneDirectPrepareResult | null>(null);
|
||||
const [storeName, setStoreName] = useState('');
|
||||
const [storeClosed, setStoreClosed] = useState(false);
|
||||
const [msg, setMsg] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [confirmCooldown, setConfirmCooldown] = useState(0);
|
||||
const [showOpenModal, setShowOpenModal] = useState(false);
|
||||
@@ -41,11 +41,10 @@ export default function PhoneRedeemPage() {
|
||||
async function prepareDirectRedeem() {
|
||||
const value = Number(amount);
|
||||
if (!Number.isFinite(value) || value <= 0) {
|
||||
setMsg('请输入有效核销金额');
|
||||
toastError('请输入有效核销金额');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setMsg('');
|
||||
try {
|
||||
const result = await request<RedeemPhoneDirectPrepareResult>('SHOP_H5', '/shop/redeem/phone/prepare-direct', {
|
||||
method: 'POST',
|
||||
@@ -54,10 +53,10 @@ export default function PhoneRedeemPage() {
|
||||
setPrepared(result);
|
||||
setConfirmCode('');
|
||||
setConfirmCooldown(60);
|
||||
setMsg(`验证码已发送至 ${result.maskedPhone},验证成功后将直接核销`);
|
||||
toastSuccess(`验证码已发送至 ${result.maskedPhone},验证成功后将直接核销`);
|
||||
} catch (e) {
|
||||
setPrepared(null);
|
||||
setMsg(e instanceof Error ? e.message : '发送验证码失败');
|
||||
toastError(e instanceof Error ? e.message : '发送验证码失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -65,7 +64,7 @@ export default function PhoneRedeemPage() {
|
||||
|
||||
async function sendConfirmSms() {
|
||||
if (!/^1\d{10}$/.test(phone.trim())) {
|
||||
setMsg('请输入正确的手机号');
|
||||
toastError('请输入正确的手机号');
|
||||
return;
|
||||
}
|
||||
if (storeClosed) {
|
||||
@@ -88,7 +87,7 @@ export default function PhoneRedeemPage() {
|
||||
await prepareDirectRedeem();
|
||||
} catch (e) {
|
||||
setShowOpenModal(false);
|
||||
setMsg(e instanceof Error ? e.message : '开启营业失败');
|
||||
toastError(e instanceof Error ? e.message : '开启营业失败');
|
||||
} finally {
|
||||
setOpening(false);
|
||||
}
|
||||
@@ -96,15 +95,14 @@ export default function PhoneRedeemPage() {
|
||||
|
||||
async function confirmRedeem() {
|
||||
if (!prepared) {
|
||||
setMsg('请先发送核销验证码');
|
||||
toastError('请先发送核销验证码');
|
||||
return;
|
||||
}
|
||||
if (!confirmCode.trim()) {
|
||||
setMsg('请输入确认验证码');
|
||||
toastError('请输入确认验证码');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setMsg('');
|
||||
try {
|
||||
const result = await request<Record<string, unknown>>('SHOP_H5', '/shop/redeem/phone/confirm', {
|
||||
method: 'POST',
|
||||
@@ -118,7 +116,7 @@ export default function PhoneRedeemPage() {
|
||||
state: { result, storeName, user: prepared.user },
|
||||
});
|
||||
} catch (e) {
|
||||
setMsg(e instanceof Error ? e.message : '核销失败');
|
||||
toastError(e instanceof Error ? e.message : '核销失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -225,8 +223,6 @@ export default function PhoneRedeemPage() {
|
||||
>
|
||||
{loading ? '正在核销…' : `确认核销 ¥${formatAmount(amountValue || 0)}`}
|
||||
</button>
|
||||
|
||||
{msg && <p className="shop-redeem-error">{msg}</p>}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -2,15 +2,12 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||
import { REDEEM_CHANNEL_LABELS, type RedeemChannel, type RedeemStatsDto } from '@dukang/shared-types';
|
||||
import { request } from '../lib/api';
|
||||
import { formatMoney, toMoneyNumber } from '../lib/money';
|
||||
import { useStorePageView } from '../lib/usePageView';
|
||||
|
||||
type RangeKey = 'today' | '7d' | '30d';
|
||||
type StatusFilter = 'all' | 'pending' | 'paid';
|
||||
|
||||
function formatMoney(n: number) {
|
||||
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
function inRange(dateStr: string, range: RangeKey) {
|
||||
const d = new Date(dateStr);
|
||||
const now = new Date();
|
||||
@@ -85,8 +82,8 @@ export default function RecordsPage() {
|
||||
}, [records, range, statusFilter]);
|
||||
|
||||
const summary = useMemo(() => {
|
||||
const totalAmount = filtered.reduce((s, r) => s + Number(r.amount || 0), 0);
|
||||
const totalSettle = filtered.reduce((s, r) => s + Number(r.settleAmount || 0), 0);
|
||||
const totalAmount = filtered.reduce((s, r) => s + toMoneyNumber(r.amount), 0);
|
||||
const totalSettle = filtered.reduce((s, r) => s + toMoneyNumber(r.settleAmount), 0);
|
||||
const rate = totalAmount > 0 ? Math.round((totalSettle / totalAmount) * 100) : 60;
|
||||
return { totalAmount, totalSettle, rate };
|
||||
}, [filtered]);
|
||||
@@ -181,8 +178,8 @@ export default function RecordsPage() {
|
||||
) : (
|
||||
<div className="shop-records-list">
|
||||
{filtered.map((r) => {
|
||||
const amount = Number(r.amount || 0);
|
||||
const settle = Number(r.settleAmount || 0);
|
||||
const amount = toMoneyNumber(r.amount);
|
||||
const settle = toMoneyNumber(r.settleAmount);
|
||||
const payout = r.payout as { paidAt?: string | null; status?: string } | undefined;
|
||||
const paid = Boolean(payout?.paidAt) || payout?.status === 'PAID' || Boolean(r.paidAt);
|
||||
const paidAt = payout?.paidAt || (typeof r.paidAt === 'string' ? r.paidAt : null);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import WeakNetFallbackPanel from '../components/WeakNetFallbackPanel';
|
||||
import { request } from '../lib/api';
|
||||
import { reportRedeemFailure } from '../lib/redeem-failure';
|
||||
import { toastError } from '../lib/toast';
|
||||
import { useStorePageView } from '../lib/usePageView';
|
||||
|
||||
function formatAmount(n: number) {
|
||||
@@ -22,7 +23,6 @@ export default function RedeemConfirmPage() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [token, setToken] = useState('');
|
||||
const [msg, setMsg] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [storeName, setStoreName] = useState('');
|
||||
const [preview, setPreview] = useState<Preview | null>(null);
|
||||
@@ -61,10 +61,9 @@ export default function RedeemConfirmPage() {
|
||||
body: JSON.stringify({ token }),
|
||||
});
|
||||
setPreview(p);
|
||||
setMsg('');
|
||||
} catch (e) {
|
||||
setPreview(null);
|
||||
setMsg(e instanceof Error ? e.message : '无法预览核销码');
|
||||
toastError(e instanceof Error ? e.message : '无法预览核销码');
|
||||
const report = await reportRedeemFailure(token, 'preview', e);
|
||||
if (report?.thresholdReached) {
|
||||
setFailCount(report.failCount);
|
||||
@@ -82,11 +81,10 @@ export default function RedeemConfirmPage() {
|
||||
|
||||
async function doConfirm() {
|
||||
if (!token.trim()) {
|
||||
setMsg('请先扫码获取核销码');
|
||||
toastError('请先扫码获取核销码');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setMsg('');
|
||||
try {
|
||||
const result = await request<Record<string, unknown>>('SHOP_H5', '/shop/redeem/confirm', {
|
||||
method: 'POST',
|
||||
@@ -95,7 +93,7 @@ export default function RedeemConfirmPage() {
|
||||
sessionStorage.setItem('lastRedeemResult', JSON.stringify(result));
|
||||
navigate('/redeem/success', { state: { result, storeName, user: preview?.user } });
|
||||
} catch (e) {
|
||||
setMsg(e instanceof Error ? e.message : '核销失败');
|
||||
toastError(e instanceof Error ? e.message : '核销失败');
|
||||
const report = await reportRedeemFailure(token, 'confirm', e);
|
||||
if (report?.thresholdReached) {
|
||||
setFailCount(report.failCount);
|
||||
@@ -126,13 +124,12 @@ export default function RedeemConfirmPage() {
|
||||
});
|
||||
setStoreClosed(false);
|
||||
setShowOpenModal(false);
|
||||
setMsg('');
|
||||
// 开张后重新拉取预览(门店已 OPEN,后端不再拦截),再继续核销
|
||||
await loadPreview();
|
||||
await doConfirm();
|
||||
} catch (e) {
|
||||
setShowOpenModal(false);
|
||||
setMsg(e instanceof Error ? e.message : '开启营业失败');
|
||||
toastError(e instanceof Error ? e.message : '开启营业失败');
|
||||
} finally {
|
||||
setOpening(false);
|
||||
}
|
||||
@@ -217,8 +214,6 @@ export default function RedeemConfirmPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{msg && <p className="shop-redeem-error">{msg}</p>}
|
||||
|
||||
{!showWeakNet && (
|
||||
<>
|
||||
<button
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { notifyRedeemSuccess } from '../lib/useRedeemSuccessBus';
|
||||
|
||||
function formatAmount(n: number) {
|
||||
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
}
|
||||
import { formatMoney, toMoneyNumber } from '../lib/money';
|
||||
|
||||
export default function RedeemSuccessPage() {
|
||||
const navigate = useNavigate();
|
||||
@@ -24,7 +21,7 @@ export default function RedeemSuccessPage() {
|
||||
const storeName = (location.state as { storeName?: string })?.storeName || '当前门店';
|
||||
const user = (location.state as { user?: { nickname?: string; phone?: string; userNo?: string } })?.user;
|
||||
const userLabel = user?.nickname || user?.phone || '—';
|
||||
const amount = Number(result?.amount ?? 0);
|
||||
const amount = toMoneyNumber(result?.amount);
|
||||
const redeemNo = String(result?.redeemNo || '—');
|
||||
const createdAt = result?.createdAt
|
||||
? new Date(String(result.createdAt)).toLocaleString('zh-CN')
|
||||
@@ -52,7 +49,7 @@ export default function RedeemSuccessPage() {
|
||||
<span className="material-symbols-outlined">check_circle</span>
|
||||
</div>
|
||||
<h2 className="shop-success-title">核销成功</h2>
|
||||
<p className="shop-success-amount">¥ {formatAmount(amount)}</p>
|
||||
<p className="shop-success-amount">¥ {formatMoney(amount)}</p>
|
||||
<p className="shop-success-sub">已入账到余额</p>
|
||||
<p className="shop-success-note">核销成功,账单通知已发送至老板手机</p>
|
||||
</section>
|
||||
|
||||
@@ -8,15 +8,12 @@ import {
|
||||
} from '@dukang/shared-types';
|
||||
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||
import { request } from '../lib/api';
|
||||
import { formatMoney, toMoneyNumber } from '../lib/money';
|
||||
import { useStorePageView } from '../lib/usePageView';
|
||||
import { useRedeemSuccessListener } from '../lib/useRedeemSuccessBus';
|
||||
|
||||
type StatusFilter = 'all' | StoreWithdrawStatus;
|
||||
|
||||
function formatMoney(n: number) {
|
||||
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
export default function WithdrawPage() {
|
||||
useStorePageView('store_withdraw_view');
|
||||
const navigate = useNavigate();
|
||||
@@ -113,13 +110,13 @@ export default function WithdrawPage() {
|
||||
<div>
|
||||
<p className="shop-records-summary-label">可提未出账余额</p>
|
||||
<p className="shop-records-summary-value">
|
||||
¥ {formatMoney(summary?.availableAmount ?? 0)}
|
||||
¥ {formatMoney(toMoneyNumber(summary?.availableAmount))}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="shop-records-summary-label">今日剩余额度</p>
|
||||
<p className="shop-records-summary-value">
|
||||
¥ {formatMoney(summary?.remainingDailyLimit ?? 0)}
|
||||
¥ {formatMoney(toMoneyNumber(summary?.remainingDailyLimit))}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -209,7 +206,7 @@ export default function WithdrawPage() {
|
||||
<div className="shop-record-amounts">
|
||||
<div>
|
||||
<p className="shop-record-amount-label">提现金额</p>
|
||||
<p className="shop-record-amount-value red">¥{formatMoney(Number(r.amount))}</p>
|
||||
<p className="shop-record-amount-value red">¥{formatMoney(toMoneyNumber(r.amount))}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="shop-record-amount-label">明细笔数</p>
|
||||
|
||||
@@ -3190,3 +3190,26 @@ header:has(> .app-page-title:only-child),
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.shop-float-toast {
|
||||
position: fixed;
|
||||
top: 28%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 10020;
|
||||
max-width: min(320px, calc(100vw - 40px));
|
||||
padding: 12px 20px;
|
||||
border-radius: 10px;
|
||||
background: rgba(0, 0, 0, 0.78);
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.shop-float-toast--error {
|
||||
background: rgba(166, 29, 36, 0.92);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user