"use client"; import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { Gift, Trophy, CheckCircle2, Timer, BarChart3, Coins, CircleHelp } from "lucide-react"; import { apiVotePoll, apiClosePoll, apiDrawLottery, apiSolveQuestion, apiReopenQuestion, type PollState, type BountyState, type LotteryState, type QuestionState, } from "@/lib/api"; import { toast } from "@/lib/toast"; import Avatar from "@/components/Avatar"; import { useAllowComments } from "@/components/CommentsPolicyProvider"; export default function PostInteractPanels({ postId, poll, bounty, lottery, loggedIn, }: { postId: number; poll?: PollState | null; bounty?: BountyState | null; lottery?: LotteryState | null; loggedIn: boolean; }) { // 问答 / 已采纳悬赏改在正文下方 AcceptedAnswerCard,此处不再放 const showBounty = !!bounty && !bounty.accepted_comment_id; if (!poll && !showBounty && !lottery) return null; return (
{poll && } {showBounty && } {lottery && }
); } function formatEndsAt(iso?: string) { if (!iso) return ""; try { return new Date(iso).toLocaleString("zh-CN"); } catch { return iso; } } /** 底栏右侧:进行中显示截止时间,结束后显示结束时间 */ function TypedFootTime({ endsAt, endedAt, ended, }: { endsAt?: string; endedAt?: string; ended?: boolean; }) { if (ended) { const t = endedAt || endsAt; if (!t) return null; return 结束时间: {formatEndsAt(t)}; } if (!endsAt) return null; return 截止时间: {formatEndsAt(endsAt)}; } function pad2(n: number) { return String(Math.max(0, n)).padStart(2, "0"); } function remainParts(endsAtMs: number, now: number) { const ms = Math.max(0, endsAtMs - now); const totalSec = Math.floor(ms / 1000); return { days: Math.floor(totalSec / 86400), hours: Math.floor((totalSec % 86400) / 3600), mins: Math.floor((totalSec % 3600) / 60), secs: totalSec % 60, expired: ms <= 0, }; } /** 未截止倒计时:最小单位秒,每秒刷新 */ function DeadlineCountdown({ endsAt, activeLabel = "距截止还剩", expiredLabel = "已到截止时间", expiredHint = "等待处理", }: { endsAt: string; activeLabel?: string; expiredLabel?: string; expiredHint?: string; }) { const endsMs = Date.parse(endsAt); const [now, setNow] = useState(null); useEffect(() => { if (!Number.isFinite(endsMs)) return; const tick = () => setNow(Date.now()); tick(); const id = window.setInterval(tick, 1000); return () => window.clearInterval(id); }, [endsMs]); if (!Number.isFinite(endsMs)) return null; if (now === null) { return ( ); } const r = remainParts(endsMs, now); return (
{r.expired ? expiredLabel : activeLabel}
{r.expired ? (

{expiredHint}

) : (
{pad2(r.days)} 天 {pad2(r.hours)} 时 {pad2(r.mins)} 分 {pad2(r.secs)} 秒
)}

截止于 {formatEndsAt(endsAt)}

); } /** 正文下方的问答状态卡(未解决 / 仅标记已解决);有采纳答案时改用 AcceptedAnswerCard */ export function QuestionStatusCard({ postId, state }: { postId: number; state: QuestionState }) { const allowComments = useAllowComments(); const router = useRouter(); const [busy, setBusy] = useState(false); const act = async (fn: () => Promise<{ post?: unknown; error?: string }>, ok: string) => { setBusy(true); try { const res = await fn(); if (res.post) { toast(ok, "ok"); router.refresh(); } else toast(res.error || "操作失败", "error"); } finally { setBusy(false); } }; const statusBadge = state.solved ? { cls: "badge-status badge-status-solved", label: "已解决" } : { cls: "badge-status badge-status-unsolved", label: "未解决" }; const footLeft = state.solved ? state.accepted_comment_id ? "已采纳答案" : "已标记解决" : "等待解答"; return (

问答

{statusBadge.label}
{(state.can_solve || state.can_reopen) && (
{state.can_solve && ( )} {state.can_reopen && ( )}
)}

{state.solved ? state.accepted_comment_id && allowComments ? ( <> 已采纳{" "} #{state.accepted_floor || "?"} ) : "作者已标记为已解决" : allowComments ? "可在评论旁采纳答案,或手动标记已解决" : "可由作者标记为已解决"}

{footLeft}
); } function PollPanel({ postId, state, loggedIn, }: { postId: number; state: PollState; loggedIn: boolean; }) { const router = useRouter(); const [sel, setSel] = useState(state.my_options || []); const [busy, setBusy] = useState(false); const showResults = state.closed || (state.my_options && state.my_options.length > 0); const hasVoted = (state.my_options?.length ?? 0) > 0; const toggle = (idx: number) => { if (!state.can_vote) return; if (state.multi) { setSel((prev) => prev.includes(idx) ? prev.filter((x) => x !== idx) : [...prev, idx] ); } else { setSel([idx]); } }; const vote = async () => { if (sel.length === 0) { toast("请先选择选项", "error"); return; } setBusy(true); try { const res = await apiVotePoll(String(postId), sel); if (res.post) { toast("投票成功", "ok"); router.refresh(); } else toast(res.error || "投票失败", "error"); } catch { toast("投票失败", "error"); } finally { setBusy(false); } }; const close = async () => { setBusy(true); try { const res = await apiClosePoll(String(postId)); if (res.post) { toast("已结束投票", "ok"); router.refresh(); } else toast(res.error || "操作失败", "error"); } finally { setBusy(false); } }; const statusBadge = state.closed ? { cls: "badge-status badge-status-closed", label: "已结束" } : { cls: "badge-status badge-status-open", label: "进行中" }; const footLeft = state.closed ? "已结束投票" : hasVoted ? "你已完成投票" : "投票进行中"; return (

投票

{statusBadge.label}
{state.can_close && (
)}

{state.multi ? "多选" : "单选"} {state.anonymous ? " · 匿名" : " · 实名"} {" · "} {state.voters} 人参与 · {state.total_votes} 票

{!state.closed && state.ends_at ? ( ) : null}
    {state.options.map((o) => { const checked = sel.includes(o.index); const namedVoters = !state.anonymous && showResults ? o.voters || [] : []; return (
  • {namedVoters.length > 0 && (
      {namedVoters.map((v) => (
    • {v.nickname || v.username}
    • ))}
    )}
  • ); })}
{(state.can_vote || (!loggedIn && !state.closed)) && (
{state.can_vote && ( )} {!loggedIn && !state.closed && ( 登录后投票 )}
)}
{footLeft}
); } function BountyPanel({ state }: { postId: number; state: BountyState }) { const allowComments = useAllowComments(); const statusBadge = state.accepted_comment_id ? { cls: "badge-status badge-status-solved", label: "已采纳" } : state.expired || state.refunded ? { cls: "badge-status badge-status-closed", label: "已过期" } : { cls: "badge-status badge-status-open", label: "进行中" }; const footLeft = state.accepted_comment_id ? "悬赏已完成" : state.expired || state.refunded ? state.can_accept ? "已过期退回 · 仍可采纳支付" : "悬赏已过期退回" : "悬赏进行中"; const bodyHint = state.accepted_answer ? "悬赏积分已发放给采纳答案" : state.accepted_comment_id ? null : state.expired || state.refunded ? state.can_accept ? "到期未采纳已自动退回;采纳将从余额扣除,支付后不可撤回" : "积分已自动退回发帖人" : state.escrowed ? "积分已托管,采纳优质回答后发放;到期未采纳自动退回" : "悬赏未托管"; return (

悬赏

{statusBadge.label}

{state.points} 积分 {bodyHint ? ` · ${bodyHint}` : null} {state.can_accept && allowComments ? " · 在评论旁点击「采纳」发放积分" : null}

{!state.accepted_comment_id && state.ends_at ? ( ) : null} {allowComments && state.accepted_comment_id > 0 && (

已采纳{" "} #{state.accepted_floor || "?"}

)}
{footLeft}
); } function LotteryPanel({ postId, state, }: { postId: number; state: LotteryState; }) { const router = useRouter(); const [busy, setBusy] = useState(false); const act = async (fn: () => Promise<{ post?: unknown; error?: string }>, ok: string) => { setBusy(true); try { const res = await fn(); if (res.post) { toast(ok, "ok"); router.refresh(); } else toast(res.error || "操作失败", "error"); } finally { setBusy(false); } }; const prizes = state.prizes?.length ? state.prizes : []; const statusBadge = state.drawn ? { cls: "badge-status badge-status-closed", label: "已开奖" } : { cls: "badge-status badge-status-open", label: "进行中" }; return (

抽奖

{statusBadge.label}
{state.can_draw && (
)}
{(state.can_draw || (state.eligible && !state.drawn)) && (

{state.can_draw ? `已有 ${state.entry_count} 名主评用户入池` : null} {state.can_draw && state.eligible && !state.drawn ? " · " : null} {state.eligible && !state.drawn ? "你已在奖池中" : null}

)} {!state.drawn && state.ends_at ? ( ) : null} {prizes.length > 0 && (
    {prizes.map((prize, i) => (
  • {prize.name} ×{prize.count}
  • ))}
)} {state.drawn && ( <>

中奖名单

{state.winners.length > 0 ? (
    {state.winners.map((w) => (
  • {w.nickname || w.username} {w.prize_name ? ( {w.prize_name} ) : null}
  • ))}
) : (

无人回帖,开奖结果为空

)} )}
{state.drawn ? "开奖完成" : "抽奖进行中"} {state.ends_at ? ( 截止时间: {formatEndsAt(state.ends_at)} ) : null}
); }