89fd333702
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>
135 lines
4.2 KiB
TypeScript
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>
|
|
);
|
|
}
|