refactor: modularize application domains
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user