178 lines
6.5 KiB
TypeScript
178 lines
6.5 KiB
TypeScript
import type { RefObject } from 'react';
|
|
import { AnimatePresence, motion } from 'motion/react';
|
|
import { ChevronLeft, ChevronRight, Sparkles, X } from 'lucide-react';
|
|
import FeedbackFormStep from './FeedbackFormStep';
|
|
import FeedbackSuccessStep from './FeedbackSuccessStep';
|
|
import FeedbackTypeStep from './FeedbackTypeStep';
|
|
import type { FeedbackStep, FeedbackType, UploadedImg } from './types';
|
|
|
|
interface Props {
|
|
canNext: boolean;
|
|
content: string;
|
|
error: string | null;
|
|
fileRef: RefObject<HTMLInputElement | null>;
|
|
maxScreenshots: number;
|
|
module: string;
|
|
open: boolean;
|
|
progress: number;
|
|
screenshots: UploadedImg[];
|
|
step: FeedbackStep;
|
|
submitting: boolean;
|
|
textareaRef: RefObject<HTMLTextAreaElement | null>;
|
|
type: FeedbackType | null;
|
|
uploading: boolean;
|
|
onAddFiles: (files: FileList | File[]) => void | Promise<void>;
|
|
onBack: () => void;
|
|
onChangeContent: (content: string) => void;
|
|
onChangeModule: (module: string) => void;
|
|
onClose: () => void;
|
|
onNext: () => void;
|
|
onRemoveScreenshot: (index: number) => void;
|
|
onSelectType: (type: FeedbackType) => void;
|
|
}
|
|
|
|
export default function FeedbackModal({
|
|
canNext,
|
|
content,
|
|
error,
|
|
fileRef,
|
|
maxScreenshots,
|
|
module,
|
|
open,
|
|
progress,
|
|
screenshots,
|
|
step,
|
|
submitting,
|
|
textareaRef,
|
|
type,
|
|
uploading,
|
|
onAddFiles,
|
|
onBack,
|
|
onChangeContent,
|
|
onChangeModule,
|
|
onClose,
|
|
onNext,
|
|
onRemoveScreenshot,
|
|
onSelectType,
|
|
}: Props) {
|
|
return (
|
|
<AnimatePresence>
|
|
{open && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
className="fixed inset-0 z-[90] bg-slate-900/40 backdrop-blur-sm flex items-end md:items-center justify-center"
|
|
>
|
|
<motion.div
|
|
initial={{ y: '100%', opacity: 0 }}
|
|
animate={{ y: 0, opacity: 1 }}
|
|
exit={{ y: '100%', opacity: 0 }}
|
|
transition={{ y: { type: 'spring', damping: 28, stiffness: 320 }, opacity: { duration: 0.18 } }}
|
|
className="bg-white w-full md:max-w-md md:rounded-3xl rounded-t-3xl shadow-2xl max-h-[92vh] overflow-hidden flex flex-col"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="flex justify-center pt-2.5 pb-1">
|
|
<div className="w-10 h-1 rounded-full bg-slate-300" />
|
|
</div>
|
|
|
|
<div className="px-4 pb-3 border-b border-slate-100">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-7 h-7 rounded-lg bg-gradient-to-br from-blue-500 to-cyan-400 flex items-center justify-center">
|
|
<Sparkles size={14} className="text-white" />
|
|
</div>
|
|
<div>
|
|
<div className="text-sm font-black text-slate-800 leading-tight">
|
|
{step === 3 ? '收到啦~' : '提个建议'}
|
|
</div>
|
|
<div className="text-[10px] text-slate-400 font-bold">
|
|
{step === 1 && '第一步 / 共 2 步'}
|
|
{step === 2 && '第二步 / 共 2 步'}
|
|
{step === 3 && '感谢你的反馈'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button onClick={onClose} className="p-1.5 -mr-1 text-slate-400 hover:text-slate-700">
|
|
<X size={18} />
|
|
</button>
|
|
</div>
|
|
<div className="h-1 bg-slate-100 rounded-full overflow-hidden">
|
|
<motion.div
|
|
initial={false}
|
|
animate={{ width: `${progress}%` }}
|
|
transition={{ type: 'spring', damping: 28, stiffness: 280 }}
|
|
className="h-full bg-gradient-to-r from-blue-500 to-cyan-400 rounded-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto px-4 py-4">
|
|
<AnimatePresence mode="wait">
|
|
{step === 1 && (
|
|
<FeedbackTypeStep
|
|
key="s1"
|
|
selectedType={type}
|
|
onSelect={onSelectType}
|
|
/>
|
|
)}
|
|
{step === 2 && (
|
|
<FeedbackFormStep
|
|
key="s2"
|
|
content={content}
|
|
error={error}
|
|
fileRef={fileRef}
|
|
maxScreenshots={maxScreenshots}
|
|
module={module}
|
|
screenshots={screenshots}
|
|
textareaRef={textareaRef}
|
|
type={type}
|
|
uploading={uploading}
|
|
onAddFiles={onAddFiles}
|
|
onChangeContent={onChangeContent}
|
|
onChangeModule={onChangeModule}
|
|
onRemoveScreenshot={onRemoveScreenshot}
|
|
/>
|
|
)}
|
|
{step === 3 && <FeedbackSuccessStep key="s3" />}
|
|
</AnimatePresence>
|
|
</div>
|
|
|
|
{step < 3 && (
|
|
<div className="px-4 py-3 border-t border-slate-100 flex items-center gap-2">
|
|
{step > 1 && (
|
|
<button
|
|
onClick={onBack}
|
|
className="px-3 py-2 rounded-xl text-[12px] font-bold text-slate-500 hover:bg-slate-50 flex items-center gap-1"
|
|
>
|
|
<ChevronLeft size={14} /> 上一步
|
|
</button>
|
|
)}
|
|
<div className="flex-1" />
|
|
<button
|
|
onClick={onNext}
|
|
disabled={!canNext || submitting}
|
|
className="px-4 py-2 rounded-xl text-[12px] font-bold bg-blue-600 text-white shadow shadow-blue-100 disabled:bg-slate-200 disabled:text-slate-400 disabled:shadow-none flex items-center gap-1"
|
|
>
|
|
{submitting ? '提交中…' : step === 2 ? '提交' : '下一步'}
|
|
{!submitting && step !== 2 && <ChevronRight size={14} />}
|
|
</button>
|
|
</div>
|
|
)}
|
|
{step === 3 && (
|
|
<div className="px-4 py-3 border-t border-slate-100">
|
|
<button
|
|
onClick={onClose}
|
|
className="w-full py-2.5 rounded-xl text-[12px] font-bold bg-blue-600 text-white shadow shadow-blue-100"
|
|
>
|
|
完成
|
|
</button>
|
|
</div>
|
|
)}
|
|
</motion.div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
}
|