149 lines
5.4 KiB
TypeScript
149 lines
5.4 KiB
TypeScript
import type { RefObject } from 'react';
|
|
import { motion } from 'motion/react';
|
|
import { ImagePlus, Loader2, X } from 'lucide-react';
|
|
import type { FeedbackType, UploadedImg } from './types';
|
|
|
|
const MODULE_LABELS: Record<string, string> = {
|
|
assets: '资产管理',
|
|
mileage: '里程管理',
|
|
energy: '能源管理',
|
|
scheduling: '智能调度',
|
|
ele: '充电导入',
|
|
'': '通用',
|
|
};
|
|
|
|
function getPlaceholder(type: FeedbackType | null): string {
|
|
if (type === 'dimension') return '比如:希望按客户/区域/日期范围 等等切片看里程数据…';
|
|
if (type === 'bug') return '比如:氢能页面 04-28 嘉燃经开站显示 153.81,但…(可粘贴截图)';
|
|
if (type === 'ux') return '比如:能不能把外部 tab 默认收起,加载快一点…';
|
|
return '随便聊聊你的想法';
|
|
}
|
|
|
|
interface Props {
|
|
content: string;
|
|
error: string | null;
|
|
fileRef: RefObject<HTMLInputElement | null>;
|
|
maxScreenshots: number;
|
|
module: string;
|
|
screenshots: UploadedImg[];
|
|
textareaRef: RefObject<HTMLTextAreaElement | null>;
|
|
type: FeedbackType | null;
|
|
uploading: boolean;
|
|
onAddFiles: (files: FileList | File[]) => void | Promise<void>;
|
|
onChangeContent: (content: string) => void;
|
|
onChangeModule: (module: string) => void;
|
|
onRemoveScreenshot: (index: number) => void;
|
|
}
|
|
|
|
export default function FeedbackFormStep({
|
|
content,
|
|
error,
|
|
fileRef,
|
|
maxScreenshots,
|
|
module,
|
|
screenshots,
|
|
textareaRef,
|
|
type,
|
|
uploading,
|
|
onAddFiles,
|
|
onChangeContent,
|
|
onChangeModule,
|
|
onRemoveScreenshot,
|
|
}: Props) {
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0, x: 12 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
exit={{ opacity: 0, x: -12 }}
|
|
transition={{ duration: 0.2 }}
|
|
className="space-y-3"
|
|
>
|
|
<div>
|
|
<p className="text-[12px] font-bold text-slate-600 mb-2">说说具体内容</p>
|
|
<textarea
|
|
ref={textareaRef}
|
|
value={content}
|
|
onChange={(e) => onChangeContent(e.target.value)}
|
|
onPaste={(e) => {
|
|
const items = e.clipboardData?.items;
|
|
if (!items) return;
|
|
const imgs: File[] = [];
|
|
for (let i = 0; i < items.length; i++) {
|
|
const it = items[i];
|
|
if (it.kind === 'file' && it.type.startsWith('image/')) {
|
|
const f = it.getAsFile();
|
|
if (f) imgs.push(f);
|
|
}
|
|
}
|
|
if (imgs.length > 0) {
|
|
e.preventDefault();
|
|
onAddFiles(imgs);
|
|
}
|
|
}}
|
|
autoFocus
|
|
rows={5}
|
|
maxLength={1000}
|
|
placeholder={getPlaceholder(type)}
|
|
className="w-full bg-slate-50 border-none rounded-xl p-3 text-[12px] text-slate-700 outline-none focus:ring-2 focus:ring-blue-500/20 resize-none"
|
|
/>
|
|
<div className="text-right text-[10px] text-slate-300 font-bold mt-1">{content.length} / 1000</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="flex items-center justify-between mb-1.5">
|
|
<p className="text-[10px] font-bold text-slate-400 uppercase">截图(可选)</p>
|
|
<span className="text-[10px] text-slate-300 font-bold">{screenshots.length}/{maxScreenshots},可粘贴</span>
|
|
</div>
|
|
<input
|
|
ref={fileRef}
|
|
type="file"
|
|
accept="image/*"
|
|
multiple
|
|
className="hidden"
|
|
onChange={(e) => { if (e.target.files) onAddFiles(e.target.files); e.target.value = ''; }}
|
|
/>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{screenshots.map((s, i) => (
|
|
<div key={i} className="relative w-16 h-16 rounded-lg overflow-hidden border border-slate-200 bg-slate-50">
|
|
<img src={s.thumbDataUrl} alt="" className="w-full h-full object-cover" />
|
|
<button
|
|
onClick={() => onRemoveScreenshot(i)}
|
|
className="absolute top-0.5 right-0.5 w-4 h-4 rounded-full bg-slate-900/70 text-white flex items-center justify-center hover:bg-slate-900"
|
|
>
|
|
<X size={10} />
|
|
</button>
|
|
</div>
|
|
))}
|
|
{screenshots.length < maxScreenshots && (
|
|
<button
|
|
onClick={() => fileRef.current?.click()}
|
|
disabled={uploading}
|
|
className="w-16 h-16 rounded-lg border border-dashed border-slate-300 bg-slate-50 hover:bg-slate-100 flex flex-col items-center justify-center gap-0.5 text-slate-400"
|
|
>
|
|
{uploading ? <Loader2 size={16} className="animate-spin" /> : <ImagePlus size={16} />}
|
|
<span className="text-[9px] font-bold">{uploading ? '上传中' : '加截图'}</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<p className="text-[10px] font-bold text-slate-400 uppercase mb-1.5">所在板块</p>
|
|
<div className="flex gap-1 flex-wrap">
|
|
{Object.entries(MODULE_LABELS).map(([key, label]) => (
|
|
<button
|
|
key={key}
|
|
onClick={() => onChangeModule(key)}
|
|
className={`px-2.5 py-1 rounded-full text-[10px] font-bold border transition-all ${module === key ? 'bg-blue-50 border-blue-200 text-blue-600' : 'bg-white border-slate-200 text-slate-500 hover:border-slate-300'}`}
|
|
>
|
|
{label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{error && <div className="text-[11px] text-rose-500 font-bold">{error}</div>}
|
|
</motion.div>
|
|
);
|
|
}
|