import { ArrowLeft, CheckCircle2, Send } from 'lucide-react'; import { useRef, useState } from 'react'; import type { ProfileFeedbackEvidenceItemInput } from '../../../packages/shared/src/contracts/runtime'; import { PlatformActionButton } from '../common/PlatformActionButton'; import { PlatformFieldLabel } from '../common/PlatformFieldLabel'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; import { PlatformSubpanel } from '../common/PlatformSubpanel'; import { PlatformTextField } from '../common/PlatformTextField'; import { PlatformUploadPreviewCard } from '../common/PlatformUploadPreviewCard'; import { PlatformUploadTile } from '../common/PlatformUploadTile'; 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 (
帮助与反馈
反馈问题
updateDescription(event.target.value)} placeholder="请填写10个字以上的问题描述以便我们提供更好的帮助,温馨提醒您请勿填写身份证号等个人隐私信息。" density="roomy" size="md" className="mt-3 min-h-[10.5rem] !rounded-none !border-0 !bg-transparent !px-0 !py-0 text-[var(--platform-text-strong)] placeholder:text-[var(--platform-text-soft)] focus:!bg-transparent focus:!ring-0" />
{descriptionLength}/{MAX_FEEDBACK_DESCRIPTION_LENGTH}
上传凭证(提供问题截图)
{evidencePreviews.map((preview) => ( removeEvidencePreview(preview.id)} /> ))} {evidencePreviews.length < MAX_FEEDBACK_EVIDENCE_COUNT ? ( ) : null}
addEvidenceFiles(event.target.files)} />
updateContactPhone(event.target.value)} placeholder="选填,如您填写则将会同步开发者与您联系" size="md" density="compact" className="mt-3 !rounded-none !border-0 !bg-transparent !px-0 !py-0 text-[var(--platform-text-strong)] placeholder:text-[var(--platform-text-soft)] focus:!bg-transparent focus:!ring-0" /> {error ? ( {error} ) : null} {submitted ? ( 反馈已提交 ) : null} {notice ? ( {notice} ) : null} {isSubmitting ? '提交中' : '提交'} showTemporaryNotice('反馈记录暂未开放')} className="self-center" > 查看反馈与投诉记录
); }