import { ArrowLeft, CheckCircle2, ImagePlus, Send, X } from 'lucide-react'; import { useRef, useState } from 'react'; import type { ProfileFeedbackEvidenceItemInput } from '../../../packages/shared/src/contracts/runtime'; const MIN_FEEDBACK_DESCRIPTION_LENGTH = 10; const MAX_FEEDBACK_DESCRIPTION_LENGTH = 200; const MAX_FEEDBACK_EVIDENCE_COUNT = 4; const MAX_CONTACT_PHONE_LENGTH = 40; const MAX_FEEDBACK_EVIDENCE_BYTES = 1_048_576; const MAX_FEEDBACK_EVIDENCE_TOTAL_BYTES = 4_194_304; export type PlatformFeedbackPayload = { description: string; contactPhone?: string | null; evidenceItems: ProfileFeedbackEvidenceItemInput[]; }; export type PlatformFeedbackViewProps = { onBack: () => void; onSubmit?: (payload: PlatformFeedbackPayload) => void | Promise; }; type EvidencePreview = { id: string; fileName: string; contentType: string; sizeBytes: number; dataUrl: string; }; function buildEvidencePreviewId(file: File, index: number) { return `${file.name}:${file.size}:${file.lastModified}:${index}`; } function readFileAsDataUrl(file: File) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { if (typeof reader.result === 'string') { resolve(reader.result); } else { reject(new Error('图片读取失败')); } }; reader.onerror = () => reject(new Error('图片读取失败')); reader.readAsDataURL(file); }); } export function PlatformFeedbackView({ onBack, onSubmit, }: PlatformFeedbackViewProps) { const evidenceInputRef = useRef(null); const [description, setDescription] = useState(''); const [contactPhone, setContactPhone] = useState(''); const [evidencePreviews, setEvidencePreviews] = useState([]); const [error, setError] = useState(null); const [notice, setNotice] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [submitted, setSubmitted] = useState(false); const descriptionLength = description.length; const showTemporaryNotice = (message: string) => { setNotice(message); window.setTimeout(() => setNotice(null), 1600); }; const updateDescription = (value: string) => { setDescription(value.slice(0, MAX_FEEDBACK_DESCRIPTION_LENGTH)); setError(null); setSubmitted(false); }; const updateContactPhone = (value: string) => { setContactPhone(value.slice(0, MAX_CONTACT_PHONE_LENGTH)); setError(null); setSubmitted(false); }; const openEvidencePicker = () => { evidenceInputRef.current?.click(); }; const addEvidenceFiles = (files: FileList | null) => { const selectedFiles = files ? Array.from(files) : []; if (evidenceInputRef.current) { evidenceInputRef.current.value = ''; } if (selectedFiles.length === 0) { return; } if (selectedFiles.some((file) => !file.type.startsWith('image/'))) { setError('反馈凭证只支持图片类型'); return; } const remainingCount = MAX_FEEDBACK_EVIDENCE_COUNT - evidencePreviews.length; if (remainingCount <= 0 || selectedFiles.length > remainingCount) { setError('最多上传四张凭证'); } const nextFiles = selectedFiles.slice(0, Math.max(remainingCount, 0)); if (nextFiles.length === 0) { return; } if (nextFiles.some((file) => file.size > MAX_FEEDBACK_EVIDENCE_BYTES)) { setError('单张反馈凭证不能超过 1MB'); return; } const currentTotalBytes = evidencePreviews.reduce( (total, preview) => total + preview.sizeBytes, 0, ); const nextTotalBytes = nextFiles.reduce( (total, file) => total + file.size, currentTotalBytes, ); if (nextTotalBytes > MAX_FEEDBACK_EVIDENCE_TOTAL_BYTES) { setError('反馈凭证总大小不能超过 4MB'); return; } Promise.all( nextFiles.map(async (file, index) => ({ id: buildEvidencePreviewId(file, evidencePreviews.length + index), fileName: file.name, contentType: file.type, sizeBytes: file.size, dataUrl: await readFileAsDataUrl(file), })), ) .then((nextPreviews) => { setEvidencePreviews((currentPreviews) => [ ...currentPreviews, ...nextPreviews, ]); setError(null); setSubmitted(false); }) .catch((readError: unknown) => { setError(readError instanceof Error ? readError.message : '图片读取失败'); }); }; const removeEvidencePreview = (id: string) => { setEvidencePreviews((currentPreviews) => currentPreviews.filter((preview) => preview.id !== id), ); setError(null); setSubmitted(false); }; const submitFeedback = () => { if (isSubmitting) { return; } const trimmedDescription = description.trim(); const trimmedContactPhone = contactPhone.trim(); if (trimmedDescription.length < MIN_FEEDBACK_DESCRIPTION_LENGTH) { setError('请填写10个字以上的问题描述'); setSubmitted(false); return; } if (trimmedDescription.length > MAX_FEEDBACK_DESCRIPTION_LENGTH) { setError('问题描述不能超过 200 字'); setSubmitted(false); return; } if (trimmedContactPhone.length > MAX_CONTACT_PHONE_LENGTH) { setError('联系电话不能超过 40 字'); setSubmitted(false); return; } if (evidencePreviews.length > MAX_FEEDBACK_EVIDENCE_COUNT) { setError('最多上传四张凭证'); setSubmitted(false); return; } setIsSubmitting(true); setError(null); void Promise.resolve( onSubmit?.({ description: trimmedDescription, contactPhone: trimmedContactPhone || null, evidenceItems: evidencePreviews.map((preview) => ({ fileName: preview.fileName, contentType: preview.contentType, sizeBytes: preview.sizeBytes, dataUrl: preview.dataUrl, })), }), ) .then(() => setSubmitted(true)) .catch((submitError: unknown) => { setSubmitted(false); setError(submitError instanceof Error ? submitError.message : '提交失败'); }) .finally(() => setIsSubmitting(false)); }; return (
帮助与反馈
反馈问题