135 lines
4.7 KiB
TypeScript
135 lines
4.7 KiB
TypeScript
"use client";
|
||
|
||
import { type ChangeEvent, useRef, useState } from "react";
|
||
import styles from "../vip.module.css";
|
||
|
||
type PromoProofModalProps = {
|
||
open: boolean;
|
||
onClose: () => void;
|
||
};
|
||
|
||
type RecognizeResult = {
|
||
ok: boolean;
|
||
nickname?: string;
|
||
avatar_image_base64?: string;
|
||
avatar_mime?: string;
|
||
confidence?: {
|
||
nickname?: number;
|
||
avatar?: number;
|
||
overall?: number;
|
||
};
|
||
message?: string;
|
||
error?: string;
|
||
};
|
||
|
||
export function PromoProofModal({ open, onClose }: PromoProofModalProps) {
|
||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||
const [confirmed, setConfirmed] = useState(false);
|
||
const [uploading, setUploading] = useState(false);
|
||
const [result, setResult] = useState<RecognizeResult | null>(null);
|
||
const [error, setError] = useState<string>("");
|
||
|
||
if (!open) return null;
|
||
|
||
const onPickFile = () => {
|
||
fileInputRef.current?.click();
|
||
};
|
||
|
||
const onFileChange = async (event: ChangeEvent<HTMLInputElement>) => {
|
||
const file = event.target.files?.[0];
|
||
if (!file) return;
|
||
|
||
setError("");
|
||
setResult(null);
|
||
setUploading(true);
|
||
try {
|
||
const form = new FormData();
|
||
form.append("image", file);
|
||
const res = await fetch("/api/promo/recognize-discount", {
|
||
method: "POST",
|
||
body: form,
|
||
});
|
||
const data = (await res.json().catch(() => ({}))) as RecognizeResult;
|
||
if (!res.ok || !data?.ok) {
|
||
setError(data?.error || data?.message || "识别失败,请换一张清晰截图再试");
|
||
} else {
|
||
setResult(data);
|
||
}
|
||
} catch (e) {
|
||
setError(e instanceof Error ? e.message : "上传失败,请稍后重试");
|
||
} finally {
|
||
setUploading(false);
|
||
event.target.value = "";
|
||
}
|
||
};
|
||
|
||
const avatarSrc =
|
||
result?.avatar_image_base64 && result?.avatar_mime
|
||
? `data:${result.avatar_mime};base64,${result.avatar_image_base64}`
|
||
: null;
|
||
|
||
return (
|
||
<div className={styles.communityModalBackdrop} role="dialog" aria-modal="true" aria-labelledby="promo-proof-title">
|
||
<div className={styles.promoModalPanel}>
|
||
<button type="button" onClick={onClose} className={styles.communityModalClose} aria-label="关闭">
|
||
×
|
||
</button>
|
||
<div className={styles.promoModalTop}>
|
||
<div className={styles.promoModalTag}>限时优惠任务</div>
|
||
<h3 id="promo-proof-title" className={styles.promoModalTitle}>推荐任意公众号文章,立减 50 元</h3>
|
||
<p className={styles.promoModalDesc}>
|
||
在手机里打开任意一篇公众号文章,点击文末推荐(心形 icon),然后上传截图。系统将识别
|
||
<strong> 头像 + 昵称 + 心形推荐痕迹 </strong>
|
||
作为优惠凭证。
|
||
</p>
|
||
<p className={styles.promoModalHint}>示例参考:页面左下角「异地世界」上方那一行(头像 / Eric / 爱心)。</p>
|
||
</div>
|
||
|
||
{!confirmed ? (
|
||
<div className={styles.promoModalActions}>
|
||
<button type="button" className={`${styles.btnPrimary} ${styles.promoPrimaryBtn}`} onClick={() => setConfirmed(true)}>
|
||
我已理解,去上传截图
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<div className={styles.promoUploadArea}>
|
||
<input
|
||
ref={fileInputRef}
|
||
type="file"
|
||
accept="image/png,image/jpeg,image/webp"
|
||
className={styles.promoFileInput}
|
||
onChange={onFileChange}
|
||
/>
|
||
<button type="button" className={styles.promoUploadBtn} onClick={onPickFile} disabled={uploading}>
|
||
{uploading ? "识别中..." : "选择手机截图并上传"}
|
||
</button>
|
||
<p className={styles.promoUploadTip}>支持 jpg/png/webp,建议宽度大于 720px 且文字清晰</p>
|
||
|
||
{error ? <p className={styles.promoError}>{error}</p> : null}
|
||
|
||
{result?.ok ? (
|
||
<div className={styles.promoResultCard}>
|
||
<p className={styles.promoResultTitle}>识别结果</p>
|
||
<div className={styles.promoResultRow}>
|
||
<div className={styles.promoAvatarWrap}>
|
||
{avatarSrc ? (
|
||
<img src={avatarSrc} alt="识别头像" className={styles.promoAvatarImg} />
|
||
) : (
|
||
<div className={styles.promoAvatarEmpty}>无头像</div>
|
||
)}
|
||
</div>
|
||
<div className={styles.promoResultMeta}>
|
||
<p className={styles.promoNicknameLabel}>昵称</p>
|
||
<p className={styles.promoNicknameValue}>{result.nickname || "未识别"}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|