From 7eb531ccca1b59f193a1ddaaa4b1d8d001f1831b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8E=86=E5=86=B0=E9=83=81-hermes=E7=89=88?= Date: Fri, 8 May 2026 12:21:55 +0800 Subject: [PATCH] feat: add platform feedback view --- .../platform-entry/PlatformFeedbackView.tsx | 322 ++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 src/components/platform-entry/PlatformFeedbackView.tsx diff --git a/src/components/platform-entry/PlatformFeedbackView.tsx b/src/components/platform-entry/PlatformFeedbackView.tsx new file mode 100644 index 00000000..231cd449 --- /dev/null +++ b/src/components/platform-entry/PlatformFeedbackView.tsx @@ -0,0 +1,322 @@ +import { ArrowLeft, CheckCircle2, Home, ImagePlus, Send, X } from 'lucide-react'; +import { useEffect, useMemo, useRef, useState } from 'react'; + +const MIN_FEEDBACK_DESCRIPTION_LENGTH = 10; +const MAX_FEEDBACK_DESCRIPTION_LENGTH = 200; +const MAX_FEEDBACK_EVIDENCE_COUNT = 4; +const MAX_CONTACT_PHONE_LENGTH = 40; + +export type PlatformFeedbackPayload = { + description: string; + contactPhone: string; + evidenceFiles: File[]; +}; + +export type PlatformFeedbackViewProps = { + onBack: () => void; + onSubmit?: (payload: PlatformFeedbackPayload) => void | Promise; +}; + +type EvidencePreview = { + id: string; + file: File; + url: string; +}; + +function buildEvidencePreviewId(file: File, index: number) { + return `${file.name}:${file.size}:${file.lastModified}:${index}`; +} + +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 evidenceFiles = useMemo( + () => evidencePreviews.map((preview) => preview.file), + [evidencePreviews], + ); + + useEffect( + () => () => { + evidencePreviews.forEach((preview) => URL.revokeObjectURL(preview.url)); + }, + [evidencePreviews], + ); + + 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) => { + if (evidenceInputRef.current) { + evidenceInputRef.current.value = ''; + } + if (!files || files.length === 0) { + return; + } + + const selectedFiles = Array.from(files).filter((file) => + file.type.startsWith('image/'), + ); + 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; + } + + const nextPreviews = nextFiles.map((file, index) => ({ + id: buildEvidencePreviewId(file, evidencePreviews.length + index), + file, + url: URL.createObjectURL(file), + })); + setEvidencePreviews((currentPreviews) => [...currentPreviews, ...nextPreviews]); + setSubmitted(false); + }; + + const removeEvidencePreview = (id: string) => { + setEvidencePreviews((currentPreviews) => { + const previewToRemove = currentPreviews.find((preview) => preview.id === id); + if (previewToRemove) { + URL.revokeObjectURL(previewToRemove.url); + } + return 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 (evidenceFiles.length > MAX_FEEDBACK_EVIDENCE_COUNT) { + setError('最多上传四张凭证'); + setSubmitted(false); + return; + } + + setIsSubmitting(true); + setError(null); + // 中文注释:首版反馈页只完成前端收集与成功态;接入后端时在 onSubmit 中替换为 API 调用。 + void Promise.resolve( + onSubmit?.({ + description: trimmedDescription, + contactPhone: trimmedContactPhone, + evidenceFiles, + }), + ) + .then(() => setSubmitted(true)) + .catch((submitError: unknown) => { + setSubmitted(false); + setError(submitError instanceof Error ? submitError.message : '提交失败'); + }) + .finally(() => setIsSubmitting(false)); + }; + + return ( +
+
+
+
+ +
+ 帮助与反馈 +
+ +
+
+ +
+
反馈问题
+ +
+ +