当我尝试编辑播放列表名称时,手风琴正在关闭,当我的鼠标在播放列表名称输入字段的输入字段上处于活动状态时,如果我按键盘上的空格键,则它是顶部播放列表往下走,最后一个就上来了。
这是为什么?
堆栈: NextJS + ShadCN UI + TailwindCSS
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { PlusCircle, Trash2, Upload } from 'lucide-react';
import { getS3ObjectUrl, listS3Objects } from '@/lib/routes';
import { useEffect, useRef, useState } from 'react';
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import Image from 'next/image';
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import Modal from "@/components/Modal";
import { useAuth } from "@/hooks/useAuth";
import { useRouter } from 'next/navigation';
import { v4 as uuidv4 } from 'uuid';
interface Playlist {
end_time: string;
images: { duration: number; url: string }[];
start_time: string;
videos: { duration: number; url: string }[];
weekdays: string;
}
interface ScheduledPlaylist {
[playlistName: string]: Playlist;
}
interface PlaylistData {
id: number;
data: ScheduledPlaylist;
}
interface PlaylistFormProps {
onSave: (schedules: PlaylistData) => void;
initialSchedules: PlaylistData;
isEditing?: boolean;
devices?: number;
}
interface MediaItem {
name: string;
url: string;
presignedUrl: string;
size: number;
}
interface S3Object {
Key: string;
Size: number;
}
const PlaylistForm: React.FC<PlaylistFormProps> = ({ onSave, initialSchedules, isEditing = false, devices = 0 }) => {
const router = useRouter();
const [schedules, setSchedules] = useState<PlaylistData>(initialSchedules);
const [error, setError] = useState<string | null>(null);
const [openItems, setOpenItems] = useState<string[]>([]);
const [isMediaModalOpen, setIsMediaModalOpen] = useState(false);
const [mediaList, setMediaList] = useState<MediaItem[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const [currentPlaylist, setCurrentPlaylist] = useState('');
const [currentMediaType, setCurrentMediaType] = useState<'images' | 'videos'>('images');
const { authToken } = useAuth();
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isEditing && initialSchedules.id) {
fetchScheduledPlaylist(initialSchedules.id);
} else {
setSchedules(initialSchedules);
setOpenItems(Object.keys(initialSchedules.data));
}
}, [initialSchedules, isEditing]);
const fetchScheduledPlaylist = async (id: number) => {
// fetchScheduledPlaylist code here
};
const addSchedule = () => {
// addSchedule code here
};
const updatePlaylistName = (oldName: string, newName: string) => {
if (oldName === newName) return;
setSchedules(prev => {
const newData = { ...prev.data };
const playlistData = newData[oldName];
delete newData[oldName];
newData[newName] = playlistData;
return { ...prev, data: newData };
});
setOpenItems(prev => prev.map(item => item === oldName ? newName : item));
};
const updateSchedule = (playlistName: string, field: keyof Playlist, value: unknown) => {
// update schedule code here
};
const updateWeekday = (playlistName: string, dayIndex: number) => {
// updateWeekday code here
};
const addMedia = (playlistName: string, type: 'images' | 'videos', url: string, duration: number = 20) => {
// addMedia code here
};
const updateMedia = (playlistName: string, type: 'images' | 'videos', index: number, field: 'url' | 'duration', value: string | number) => {
const updatedMedia = schedules.data[playlistName][type].map((item, i) =>
i === index ? { ...item, [field]: value } : item
);
updateSchedule(playlistName, type, updatedMedia);
};
const removeMedia = (playlistName: string, type: 'images' | 'videos', index: number) => {
const updatedMedia = schedules.data[playlistName][type].filter((_, i) => i !== index);
updateSchedule(playlistName, type, updatedMedia);
};
const deletePlaylist = (playlistName: string) => {
if (!confirm(`Are you sure you want to delete the playlist "${playlistName}"?`)) return;
const newSchedules = { ...schedules };
delete newSchedules.data[playlistName];
setSchedules(newSchedules);
};
const deleteSetup = async () => {
if (!confirm("Are you sure you want to delete this entire setup?")) return;
try {
const response = await fetch(`/api/proxy/scheduled_playlists/${initialSchedules.id}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${authToken}`,
},
});
if (response.ok) {
router.push('/dashboard/setups');
} else {
setError("Failed to delete setup");
}
} catch (error) {
console.error("Error deleting setup:", error);
setError("Error deleting setup");
}
};
const savePlaylist = async () => {
// savePlaylist code here
};
const fetchMediaList = async () => {
// fetchMediaList code here
};
const handleUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
// handleUpload code here
};
const renderMediaSection = (playlistName: string, type: 'images' | 'videos') => {
return (
// code here
};
return (
<div className="mt-8 space-y-6">
<h2 className="text-2xl font-bold">Playlists</h2>
{error && (
<Alert variant="destructive">
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Accordion
type="multiple"
value={openItems}
onValueChange={setOpenItems}
className="w-full"
>
{Object.entries(schedules.data).map(([playlistName, playlist]) => (
<AccordionItem value={playlistName} key={uuidv4()}>
<AccordionTrigger>
{playlistName}
</AccordionTrigger>
<AccordionContent>
<div className="space-y-4">
<div>
<Label>Playlist Name</Label>
<Input
value={playlistName}
onChange={(e) => updatePlaylistName(playlistName, e.target.value)}
className="mt-1 bg-white text-black"
/>
</div>
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
deletePlaylist(playlistName);
}}
className="hover:bg-red-100 p-2 mr-2"
>
<Trash2 size={16} className="text-red-500" />
</Button>
</div>
<div className="space-y-4">
<div className="flex space-x-4">
<div className="w-1/2">
<Label>Start Time</Label>
<Input
type="time"
value={playlist.start_time}
onChange={(e) => updateSchedule(playlistName, 'start_time', e.target.value)}
className="mt-1 bg-white text-black"
/>
</div>
<div className="w-1/2">
<Label>End Time</Label>
<Input
type="time"
value={playlist.end_time}
onChange={(e) => updateSchedule(playlistName, 'end_time', e.target.value)}
className="mt-1 bg-white text-black"
/>
</div>
</div>
<div>
<Label className="mb-2 block">Weekdays</Label>
<div className="flex space-x-4 mt-2">
{['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map((day, index) => (
<div key={day} className="flex items-center">
<Checkbox
id={`${day}-${playlistName}`}
checked={playlist.weekdays[index] === '1'}
onCheckedChange={() => updateWeekday(playlistName, index)}
/>
<Label htmlFor={`${day}-${playlistName}`} className="ml-1">{day}</Label>
</div>
))}
</div>
</div>
{renderMediaSection(playlistName, 'images')}
{renderMediaSection(playlistName, 'videos')}
</div>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
<Button onClick={addSchedule} variant="outline" className="mt-4 dark:border-white border-black">
<PlusCircle size={20} className="mr-2" /> Add Another Playlist
</Button>
<div className="flex space-x-12 items-center">
<Button onClick={savePlaylist} className="min-w-fit py-4 text-lg mt-8 bg-green-600 hover:bg-green-800 text-white">
{isEditing ? "Update Setup" : "Save Setup"}
</Button>
{isEditing && (
<Button onClick={deleteSetup} className="min-w-fit py-4 text-lg mt-8 bg-red-600 hover:bg-red-800 text-white">
Delete Setup
</Button>
)}
</div>
<Modal isOpen={isMediaModalOpen} onClose={() => setIsMediaModalOpen(false)}>
<div className="p-4">
<h2 className="text-xl font-bold mb-4">Select Media</h2>
<div className="flex items-center mb-4">
<Input
type="text"
placeholder="Search media..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="flex-grow mr-2"
/>
<input
type="file"
ref={fileInputRef}
style={{ display: 'none' }}
onChange={handleUpload}
accept="image/*,video/*"
/>
<Button
variant="outline"
size="icon"
title="Upload media"
onClick={() => fileInputRef.current?.click()}
>
<Upload size={20} />
</Button>
</div>
<div className="space-y-4">
<div>
<h3 className="text-lg font-medium mb-2">Images</h3>
<div className="grid grid-cols-3 gap-4 max-h-[30vh] overflow-y-auto">
{mediaList
.filter(media =>
media.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
media.name.match(/\.(jpeg|jpg|gif|png)$/i)
)
.map((media) => (
<button
type="button"
key={media.name}
className="cursor-pointer text-left"
onClick={() => handleMediaSelection(media)}
>
<Image
src={media.presignedUrl}
alt={media.name}
width={100}
height={100}
className="w-full h-24 object-cover"
/>
<p className="mt-2 text-sm truncate">{media.name}</p>
<p className="text-xs text-gray-500">{(media.size / 1024 / 1024).toFixed(2)} MB</p>
</button>
))}
</div>
</div>
<div>
<h3 className="text-lg font-medium mb-2">Videos</h3>
<div className="grid grid-cols-3 gap-4 max-h-[30vh] overflow-y-auto">
{mediaList
.filter(media =>
media.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
media.name.match(/\.(mp4|webm|ogg)$/i)
)
.map((media) => (
<button
type="button"
key={media.name}
className="cursor-pointer text-left"
onClick={() => handleMediaSelection(media)}
>
<video src={media.presignedUrl} className="w-full h-24 object-cover">
<track kind="captions" />
Your browser does not support the video tag.
</video>
<p className="mt-2 text-sm truncate">{media.name}</p>
<p className="text-xs text-gray-500">{(media.size / 1024 / 1024).toFixed(2)} MB</p>
</button>
))}
</div>
</div>
</div>
</div>
</Modal>
</div>
);
};
export default PlaylistForm;
我尝试将输入字段移到手风琴内容之外,但它不起作用,请帮助我找到该错误。
提前致谢,感谢帮助
这似乎是一个传播错误。试试这个:
<AccordionContent>
<div className="space-y-4">
<div>
<Label>Playlist Name</Label>
<Input
value={playlistName}
onChange={(e) => updatePlaylistName(playlistName, e.target.value)}
onKeyDown={(e) => {
if (e.key === ' ') {
e.stopPropagation();
}
}}
className="mt-1 bg-white text-black"
/>
</div>
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
deletePlaylist(playlistName);
}}
className="hover:bg-red-100 p-2 mr-2"
>
<Trash2 size={16} className="text-red-500" />
</Button>
</div>
<div className="space-y-4">
<div className="flex space-x-4">
<div className="w-1/2">
<Label>Start Time</Label>
<Input
type="time"
value={playlist.start_time}
onChange={(e) => updateSchedule(playlistName, 'start_time', e.target.value)}
className="mt-1 bg-white text-black"
/>
</div>
<div className="w-1/2">
<Label>End Time</Label>
<Input
type="time"
value={playlist.end_time}
onChange={(e) => updateSchedule(playlistName, 'end_time', e.target.value)}
className="mt-1 bg-white text-black"
/>
</div>
</div>
<div>
<Label className="mb-2 block">Weekdays</Label>
<div className="flex space-x-4 mt-2">
{['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].map((day, index) => (
<div key={day} className="flex items-center">
<Checkbox
id={`${day}-${playlistName}`}
checked={playlist.weekdays[index] === '1'}
onCheckedChange={() => updateWeekday(playlistName, index)}
/>
<Label htmlFor={`${day}-${playlistName}`} className="ml-1">{day}</Label>
</div>
))}
</div>
</div>
{renderMediaSection(playlistName, 'images')}
{renderMediaSection(playlistName, 'videos')}
</div>
问题是,当您单击输入时,您也会触发手风琴,因此您需要在单击输入时停止事件传播。