Files
dukang/apps/h5-shop/src/components/WeakNetFallbackPanel.tsx
T
2026-07-12 12:00:55 +08:00

160 lines
5.1 KiB
TypeScript

import { useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { request } from '../lib/api';
import { uploadRedeemPendingPhoto } from '../lib/upload';
import { isWechatEnv, weixinSdk } from '../lib/weixin';
import type { RedeemPendingSubmitResult } from '@dukang/shared-types';
type Props = {
redeemToken: string;
failCount: number;
};
export default function WeakNetFallbackPanel({ redeemToken, failCount }: Props) {
const navigate = useNavigate();
const inputRef = useRef<HTMLInputElement>(null);
const [uploading, setUploading] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [previewUrl, setPreviewUrl] = useState('');
const [photoResourceId, setPhotoResourceId] = useState('');
const [result, setResult] = useState<RedeemPendingSubmitResult | null>(null);
const [msg, setMsg] = useState('');
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 : '上传失败');
} finally {
setUploading(false);
}
}
async function pickPhoto() {
setMsg('');
if (isWechatEnv()) {
try {
setUploading(true);
await weixinSdk.init();
const files = await weixinSdk.chooseImages({
count: 1,
sourceType: ['album', 'camera'],
});
if (files?.[0]) {
await handleFile(files[0]);
}
} catch (e) {
const text = e instanceof Error ? e.message : '选图失败';
if (!/cancel/i.test(text)) setMsg(text);
} finally {
setUploading(false);
}
return;
}
inputRef.current?.click();
}
async function submitPending() {
if (!photoResourceId) {
setMsg('请先拍摄或上传核销码照片');
return;
}
setSubmitting(true);
setMsg('');
try {
const res = await request<RedeemPendingSubmitResult>('SHOP_H5', '/shop/redeem/pending', {
method: 'POST',
body: JSON.stringify({
token: redeemToken,
photoResourceId,
failCount,
}),
});
setResult(res);
} catch (e) {
setMsg(e instanceof Error ? e.message : '提交失败');
} finally {
setSubmitting(false);
}
}
function copyText(text: string) {
void navigator.clipboard?.writeText(text).then(
() => setMsg('已复制'),
() => setMsg('复制失败,请手动长按复制'),
);
}
return (
<section className="shop-weaknet-panel" role="alert">
<h3 className="headline-md" style={{ marginBottom: 8 }}>弱网核销兜底</h3>
<p className="body-md" style={{ marginBottom: 12, lineHeight: 1.5 }}>
扫码核销网络失败已达 {failCount} 次。请记录用户手机号尝试「手机号核销」,
或拍下用户核销码照片提交客服人工处理。
</p>
<div className="shop-weaknet-token">
<span className="label-md text-muted">核销码 ID(追查用)</span>
<button type="button" className="shop-weaknet-copy" onClick={() => copyText(redeemToken)}>
{redeemToken}
</button>
</div>
{result ? (
<div className="shop-weaknet-success">
<p className="body-md">已提交客服,待处理单号:</p>
<button type="button" className="shop-weaknet-copy" onClick={() => copyText(result.pendingNo)}>
{result.pendingNo}
</button>
<p className="label-md text-muted" style={{ marginTop: 8 }}>
请告知用户保留核销码编号,客服将尽快人工补核销。
</p>
</div>
) : (
<>
<input
ref={inputRef}
type="file"
accept="image/*"
capture="environment"
hidden
onChange={(e) => {
const file = e.target.files?.[0];
if (file) void handleFile(file);
}}
/>
{previewUrl && (
<img src={previewUrl} alt="核销码照片预览" className="shop-weaknet-preview" />
)}
<div className="shop-weaknet-actions">
<button type="button" className="shop-redeem-confirm-btn" disabled={uploading} onClick={() => void pickPhoto()}>
{uploading ? '上传中…' : previewUrl ? '重新拍照' : '拍照 / 选图'}
</button>
<button
type="button"
className="shop-redeem-confirm-btn"
disabled={submitting || !photoResourceId}
onClick={() => void submitPending()}
>
{submitting ? '提交中…' : '提交客服人工核销'}
</button>
<button
type="button"
className="shop-btn-outline"
onClick={() => navigate('/redeem/phone')}
>
去手机号核销
</button>
</div>
</>
)}
{msg && <p className="shop-redeem-error" style={{ marginTop: 12 }}>{msg}</p>}
</section>
);
}