Files
dukang/apps/h5-user/src/pages/RedeemSuccessPage.tsx
T
jacy 89fd333702 feat(analytics): persona logging upgrade and Sentry system config
Add client-logging SDK, expanded event taxonomy, API observability, admin domain events UI, and move SENTRY_DSN to HQ system settings with @sentry/node bootstrap after config preload.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 22:30:02 +08:00

135 lines
4.2 KiB
TypeScript

import { useMemo, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import PageHeader from '@dukang/shared-ui/PageHeader';
import { request } from '../lib/api';
import { usePageView } from '../lib/usePageView';
type RedeemRecord = {
id: string;
redeemNo: string;
amount: number;
storeId: string;
storeName: string;
createdAt: string;
};
function formatMoney(amount: number) {
return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function StarRating({
label,
value,
onChange,
}: {
label: string;
value: number;
onChange: (score: number) => void;
}) {
return (
<div className="redeem-rating-row">
<span className="redeem-rating-label">{label}</span>
<div className="redeem-star-row" role="group" aria-label={label}>
{[1, 2, 3, 4, 5].map((score) => (
<button
key={score}
type="button"
className={`redeem-star-btn${score <= value ? ' active' : ''}`}
aria-label={`${score} 星`}
onClick={() => onChange(score)}
>
</button>
))}
</div>
</div>
);
}
export default function RedeemSuccessPage() {
usePageView('redeem_success_view');
const navigate = useNavigate();
const location = useLocation();
const [serviceScore, setServiceScore] = useState(5);
const [envScore, setEnvScore] = useState(5);
const [loading, setLoading] = useState(false);
const record = useMemo<RedeemRecord | null>(() => {
const fromState = (location.state as { record?: RedeemRecord })?.record;
if (fromState) return fromState;
try {
const cached = sessionStorage.getItem('lastRedeemResult');
return cached ? (JSON.parse(cached) as RedeemRecord) : null;
} catch {
return null;
}
}, [location.state]);
const amount = Number(record?.amount ?? 0);
const storeName = record?.storeName || '门店';
const redeemNo = record?.redeemNo || '—';
const redeemedAt = record?.createdAt
? new Date(record.createdAt).toLocaleString('zh-CN')
: new Date().toLocaleString('zh-CN');
async function submit() {
setLoading(true);
try {
const redeemRecordId = record?.id || sessionStorage.getItem('lastRedeemRecordId');
if (redeemRecordId) {
await request('USER_H5', '/redeem/ratings', {
method: 'POST',
body: JSON.stringify({
redeemRecordId,
serviceScore,
envScore,
}),
});
}
} catch {
/* 评价失败不阻塞返回 */
} finally {
setLoading(false);
sessionStorage.removeItem('lastRedeemRecordId');
sessionStorage.removeItem('lastRedeemResult');
navigate('/benefit', { replace: true });
}
}
return (
<div className="page-no-tab redeem-success-page">
<PageHeader title="核销成功" onBack={() => navigate('/benefit', { replace: true })} />
<div className="redeem-success-hero">
<div className="success-icon"></div>
<h2 className="headline-lg text-primary">核销成功</h2>
<p className="redeem-success-amount">¥ {formatMoney(amount)}</p>
<p className="text-variant body-md">已在 {storeName} 完成核销</p>
</div>
<div className="card redeem-success-details">
<div className="redeem-success-detail-row">
<span>核销门店</span>
<span>{storeName}</span>
</div>
<div className="redeem-success-detail-row">
<span>核销时间</span>
<span>{redeemedAt}</span>
</div>
<div className="redeem-success-detail-row">
<span>核销单号</span>
<span className="redeem-success-mono">{redeemNo}</span>
</div>
</div>
<div className="card redeem-success-rating">
<p className="redeem-success-rating-title">为门店服务评分</p>
<StarRating label="服务态度" value={serviceScore} onChange={setServiceScore} />
<StarRating label="用餐环境" value={envScore} onChange={setEnvScore} />
<button type="button" className="btn btn-primary btn-block" disabled={loading} onClick={submit}>
{loading ? '提交中...' : '完成'}
</button>
</div>
</div>
);
}