"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";
import { postPath, userPath, commentAnchor } from "@/lib/urlStyle";
import { useUrlStyle } from "@/components/SiteBrandProvider";
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 (
{activeLabel}
{["天", "时", "分", "秒"].map((suf) => (
--
{suf}
))}
截止于 {formatEndsAt(endsAt)}
);
}
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 urlStyle = useUrlStyle();
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 (
{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 urlStyle = useUrlStyle();
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 && (
)}
{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}
);
}