"use client"; import { useEffect, useState, useCallback } from "react"; import { useActiveClass } from "@/hooks/use-active-class"; import { useAuth } from "@/hooks/use-auth"; import { fetchAPI, postAPI, putAPI, deleteAPI, getErrorMessage } from "@/lib/api"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Badge } from "@/components/ui/badge"; import { Switch } from "@/components/ui/switch"; import { Label } from "@/components/ui/label"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { ErrorState } from "@/components/error-state"; import { Pagination } from "@/components/pagination"; import { toast } from "sonner"; import type { Vote, PageResponse } from "@/lib/types"; import { hasClassPermission } from "@/lib/permissions"; export default function VotesPage() { const { activeClassId } = useActiveClass(); const { user } = useAuth(); // List state const [votes, setVotes] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [page, setPage] = useState(1); const [totalPages, setTotalPages] = useState(1); // Create dialog state const [createOpen, setCreateOpen] = useState(false); const [formTitle, setFormTitle] = useState(""); const [formDesc, setFormDesc] = useState(""); const [formVoteType, setFormVoteType] = useState<"single" | "multiple">("single"); const [formAnonymous, setFormAnonymous] = useState(false); const [formMaxChoices, setFormMaxChoices] = useState(2); const [formDeadline, setFormDeadline] = useState(""); const [formOptions, setFormOptions] = useState(["", ""]); const [submitting, setSubmitting] = useState(false); // Detail dialog state const [detailOpen, setDetailOpen] = useState(false); const [detailVote, setDetailVote] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const [selectedOptions, setSelectedOptions] = useState([]); const [submittingVote, setSubmittingVote] = useState(false); // Delete confirm state const [deleteTarget, setDeleteTarget] = useState(null); const loadVotes = useCallback(async () => { if (!activeClassId) return; setError(null); setLoading(true); try { const res = await fetchAPI>("/api/votes/", { class_id: String(activeClassId), page: String(page), page_size: "10", }); setVotes(res.items || []); setTotalPages(res.total_pages || 1); } catch (err: unknown) { setError(getErrorMessage(err, "加载失败")); } finally { setLoading(false); } }, [activeClassId, page]); useEffect(() => { loadVotes(); }, [loadVotes]); // Reset page when class changes useEffect(() => { setPage(1); }, [activeClassId]); // ---------- Create ---------- const resetCreateForm = () => { setFormTitle(""); setFormDesc(""); setFormVoteType("single"); setFormAnonymous(false); setFormMaxChoices(2); setFormDeadline(""); setFormOptions(["", ""]); }; const handleCreate = async () => { if (!formTitle.trim()) { toast.error("请输入投票标题"); return; } const validOptions = formOptions.filter((o) => o.trim()); if (validOptions.length < 2) { toast.error("至少需要两个选项"); return; } if (formVoteType === "multiple" && formMaxChoices < 2) { toast.error("多选投票至少允许选择2项"); return; } if (formVoteType === "multiple" && formMaxChoices > validOptions.length) { toast.error("最多可选择数不能超过选项总数"); return; } setSubmitting(true); try { await postAPI(`/api/votes/?class_id=${activeClassId}`, { title: formTitle.trim(), description: formDesc.trim() || null, vote_type: formVoteType, is_anonymous: formAnonymous, max_choices: formVoteType === "multiple" ? formMaxChoices : 1, deadline: formDeadline || null, options: validOptions, }); toast.success("投票已创建"); setCreateOpen(false); resetCreateForm(); loadVotes(); } catch (err: unknown) { toast.error(getErrorMessage(err, "创建失败")); } finally { setSubmitting(false); } }; const addOption = () => setFormOptions([...formOptions, ""]); const removeOption = (index: number) => { if (formOptions.length <= 2) return; setFormOptions(formOptions.filter((_, i) => i !== index)); }; const updateOption = (index: number, value: string) => { const updated = [...formOptions]; updated[index] = value; setFormOptions(updated); }; // ---------- Detail ---------- const openDetail = async (voteId: number) => { setDetailOpen(true); setDetailLoading(true); setSelectedOptions([]); try { const data = await fetchAPI(`/api/votes/${voteId}`); setDetailVote(data); } catch (err: unknown) { toast.error(getErrorMessage(err, "加载投票详情失败")); setDetailOpen(false); } finally { setDetailLoading(false); } }; const handleSubmitVote = async () => { if (!detailVote) return; if (selectedOptions.length === 0) { toast.error("请至少选择一个选项"); return; } if (detailVote.vote_type === "multiple" && selectedOptions.length > detailVote.max_choices) { toast.error(`最多只能选择${detailVote.max_choices}项`); return; } setSubmittingVote(true); try { await postAPI(`/api/votes/${detailVote.id}/submit`, { option_ids: selectedOptions, }); toast.success("投票成功"); // Refresh detail const data = await fetchAPI(`/api/votes/${detailVote.id}`); setDetailVote(data); setSelectedOptions([]); loadVotes(); } catch (err: unknown) { toast.error(getErrorMessage(err, "投票失败")); } finally { setSubmittingVote(false); } }; const handleCloseVote = async (voteId: number) => { try { await putAPI(`/api/votes/${voteId}/close`); toast.success("投票已关闭"); setDetailOpen(false); loadVotes(); } catch (err: unknown) { toast.error(getErrorMessage(err, "关闭失败")); } }; const handleDelete = async (id: number) => { try { await deleteAPI(`/api/votes/${id}`); toast.success("已删除"); setDeleteTarget(null); setDetailOpen(false); loadVotes(); } catch (err: unknown) { toast.error(getErrorMessage(err, "删除失败")); } }; const canManage = (vote: Vote) => user && ( hasClassPermission(user, "vote_manage", vote.class_id) || user.id === vote.creator_id ); // ---------- Render helpers ---------- const renderStatusBadge = (status: Vote["status"]) => { if (status === "open") { return 进行中; } return 已结束; }; const formatDeadline = (deadline: string | null) => { if (!deadline) return "无截止时间"; return new Date(deadline).toLocaleString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", }); }; const getPercentage = (count: number, total: number) => { if (total === 0) return 0; return Math.round((count / total) * 100); }; return (
{/* Header */}

投票

班级投票与调查

{ setCreateOpen(open); if (!open) resetCreateForm(); }}> 创建投票
{/* Title */}
setFormTitle(e.target.value)} />
{/* Description */}