174 lines
8.3 KiB
TypeScript
174 lines
8.3 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { DataService } from '../services/dataService';
|
|
import { GeminiService } from '../services/geminiService';
|
|
import { MaterialType, UserDTO } from '../types';
|
|
import { X, Wand2, UploadCloud } from 'lucide-react';
|
|
import { useToast } from './ToastProvider';
|
|
|
|
interface CreateModalProps {
|
|
onClose: () => void;
|
|
onSuccess: () => void;
|
|
}
|
|
|
|
export const CreateModal: React.FC<CreateModalProps> = ({ onClose, onSuccess }) => {
|
|
const toast = useToast();
|
|
const [formData, setFormData] = useState({
|
|
title: '',
|
|
description: '',
|
|
type: MaterialType.CODE,
|
|
codeSnippet: '',
|
|
tags: [] as string[]
|
|
});
|
|
const [file, setFile] = useState<File | null>(null);
|
|
const [currentUser, setCurrentUser] = useState<UserDTO | null>(null);
|
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
|
|
useEffect(() => {
|
|
DataService.getCurrentUser().then(setCurrentUser).catch(() => setCurrentUser(null));
|
|
}, []);
|
|
|
|
const handleAutoGenerate = async () => {
|
|
if (!formData.codeSnippet) return;
|
|
setIsProcessing(true);
|
|
const { description, tags } = await GeminiService.analyzeCode(formData.codeSnippet);
|
|
setFormData(prev => ({
|
|
...prev,
|
|
description,
|
|
tags: [...prev.tags, ...tags]
|
|
}));
|
|
setIsProcessing(false);
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setIsProcessing(true);
|
|
try {
|
|
if (formData.type === MaterialType.CODE) {
|
|
await DataService.createMaterial(formData);
|
|
} else if (formData.type === MaterialType.ASSET_ZIP) {
|
|
if (!file) throw new Error('Please select ZIP file');
|
|
if (file.size > 3 * 1024 * 1024) throw new Error('ZIP must be ≤ 3MB');
|
|
await DataService.uploadZip(file, { title: formData.title, description: formData.description, tags: formData.tags });
|
|
} else if (formData.type === MaterialType.VIDEO) {
|
|
if (!(currentUser?.role === 'MANAGER' || currentUser?.role === 'ADMIN')) throw new Error('Manager role required');
|
|
if (!file) throw new Error('Please select video file');
|
|
await DataService.uploadVideo(file, { title: formData.title, description: formData.description, tags: formData.tags });
|
|
}
|
|
onSuccess();
|
|
} catch (err: any) {
|
|
toast.error(err.message || 'Upload failed');
|
|
} finally {
|
|
setIsProcessing(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
|
|
<div className="bg-cyber-dark border border-cyber-neon/30 w-full max-w-2xl rounded-xl shadow-2xl relative overflow-hidden">
|
|
|
|
{/* Decorative Header */}
|
|
<div className="h-1 w-full bg-gradient-to-r from-cyber-neon via-cyber-blue to-cyber-pink"></div>
|
|
|
|
<div className="p-8">
|
|
<div className="flex justify-between items-center mb-8">
|
|
<h2 className="text-2xl font-mono font-bold text-white tracking-tight">UPLOAD_NEW_PROTOCOL</h2>
|
|
<button onClick={onClose} className="text-gray-500 hover:text-white"><X /></button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
|
|
{/* Type Selection */}
|
|
<div className="flex gap-4">
|
|
{Object.values(MaterialType).map(t => {
|
|
const isVideo = t === MaterialType.VIDEO;
|
|
const allowed = !isVideo || (currentUser?.role === 'MANAGER' || currentUser?.role === 'ADMIN');
|
|
return (
|
|
<button
|
|
key={t}
|
|
type="button"
|
|
onClick={() => {
|
|
if (!allowed) { toast.error('Manager role required'); return; }
|
|
setFormData({ ...formData, type: t });
|
|
}}
|
|
className={`flex-1 py-3 text-xs font-mono border ${formData.type === t ? 'bg-cyber-neon/20 border-cyber-neon text-cyber-neon' : 'border-cyber-panel text-gray-500 hover:border-gray-500'} ${!allowed ? 'opacity-50 cursor-not-allowed' : ''}`}
|
|
>
|
|
{t}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Title */}
|
|
<div>
|
|
<label className="block text-xs font-mono text-gray-400 mb-1">TITLE_IDENTIFIER</label>
|
|
<input
|
|
required
|
|
className="w-full bg-black/50 border border-cyber-panel p-3 text-white focus:border-cyber-neon focus:outline-none"
|
|
value={formData.title}
|
|
onChange={e => setFormData({...formData, title: e.target.value})}
|
|
/>
|
|
</div>
|
|
|
|
{/* Content Input Based on Type */}
|
|
{formData.type === MaterialType.CODE ? (
|
|
<div className="relative">
|
|
<label className="block text-xs font-mono text-gray-400 mb-1">SOURCE_CODE</label>
|
|
<textarea
|
|
required
|
|
className="w-full h-40 bg-black/50 border border-cyber-panel p-3 text-xs font-mono text-cyber-blue focus:border-cyber-blue focus:outline-none"
|
|
value={formData.codeSnippet}
|
|
onChange={e => setFormData({...formData, codeSnippet: e.target.value})}
|
|
/>
|
|
{/* AI Magic Button */}
|
|
<button
|
|
type="button"
|
|
onClick={handleAutoGenerate}
|
|
disabled={!formData.codeSnippet || isProcessing}
|
|
className="absolute bottom-2 right-2 px-3 py-1 bg-cyber-blue/20 text-cyber-blue border border-cyber-blue/50 text-xs font-mono flex items-center gap-2 hover:bg-cyber-blue hover:text-black transition-colors"
|
|
>
|
|
<Wand2 size={12} /> {isProcessing ? 'SCANNING...' : 'AI_AUTO_FILL'}
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div>
|
|
<label className="block text-xs font-mono text-gray-400 mb-1">LOCAL_FILE {formData.type === MaterialType.ASSET_ZIP ? '(ZIP ≤ 3MB)' : '(Video ≤ 3MB)'}</label>
|
|
<label className="relative w-full block bg-black/50 border border-cyber-panel p-3 text-gray-400 hover:border-cyber-neon cursor-pointer transition-colors">
|
|
<input
|
|
required
|
|
type="file"
|
|
accept={formData.type === MaterialType.ASSET_ZIP ? '.zip,application/zip' : 'video/*,.mp4,.webm,.mov'}
|
|
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
|
onChange={e => setFile(e.target.files?.[0] || null)}
|
|
/>
|
|
<span className="flex items-center gap-2">
|
|
<UploadCloud size={16} />
|
|
{file ? file.name : '点击或拖拽文件到此处'}
|
|
</span>
|
|
</label>
|
|
</div>
|
|
)}
|
|
|
|
{/* Description */}
|
|
<div>
|
|
<label className="block text-xs font-mono text-gray-400 mb-1">DESCRIPTION</label>
|
|
<textarea
|
|
className="w-full h-24 bg-black/50 border border-cyber-panel p-3 text-sm text-gray-300 focus:border-cyber-neon focus:outline-none"
|
|
value={formData.description}
|
|
onChange={e => setFormData({...formData, description: e.target.value})}
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={isProcessing}
|
|
className="w-full py-4 bg-cyber-neon text-black font-bold font-mono tracking-widest hover:bg-white transition-colors flex items-center justify-center gap-2"
|
|
>
|
|
{isProcessing ? 'UPLOADING...' : <><UploadCloud size={20} /> INITIALIZE_UPLOAD</>}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|