683 lines
22 KiB
TypeScript
683 lines
22 KiB
TypeScript
"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 (
|
||
<div className="j13-interact-below space-y-4">
|
||
{poll && <PollPanel postId={postId} state={poll} loggedIn={loggedIn} />}
|
||
{showBounty && <BountyPanel postId={postId} state={bounty!} />}
|
||
{lottery && <LotteryPanel postId={postId} state={lottery} />}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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 <span className="j13-typed-foot-time">结束时间: {formatEndsAt(t)}</span>;
|
||
}
|
||
if (!endsAt) return null;
|
||
return <span className="j13-typed-foot-time">截止时间: {formatEndsAt(endsAt)}</span>;
|
||
}
|
||
|
||
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<number | null>(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 (
|
||
<div className="j13-deadline-countdown" aria-hidden="true">
|
||
<div className="j13-deadline-countdown-label">
|
||
<Timer size={15} strokeWidth={2.25} aria-hidden />
|
||
{activeLabel}
|
||
</div>
|
||
<div className="j13-deadline-countdown-digits">
|
||
{["天", "时", "分", "秒"].map((suf) => (
|
||
<span key={suf} className={`j13-deadline-countdown-unit${suf === "秒" ? " is-sec" : ""}`}>
|
||
<span className="j13-deadline-countdown-num tabular-nums">--</span>
|
||
<span className="j13-deadline-countdown-suf">{suf}</span>
|
||
</span>
|
||
))}
|
||
</div>
|
||
<p className="j13-deadline-countdown-ends">截止于 {formatEndsAt(endsAt)}</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const r = remainParts(endsMs, now);
|
||
|
||
return (
|
||
<div
|
||
className={`j13-deadline-countdown${r.expired ? " is-expired" : ""}`}
|
||
role="timer"
|
||
aria-live="polite"
|
||
aria-atomic="true"
|
||
>
|
||
<div className="j13-deadline-countdown-label">
|
||
<Timer size={15} strokeWidth={2.25} aria-hidden />
|
||
{r.expired ? expiredLabel : activeLabel}
|
||
</div>
|
||
{r.expired ? (
|
||
<p className="j13-deadline-countdown-expired">{expiredHint}</p>
|
||
) : (
|
||
<div
|
||
className="j13-deadline-countdown-digits"
|
||
aria-label={`还剩 ${r.days} 天 ${r.hours} 时 ${r.mins} 分 ${r.secs} 秒`}
|
||
>
|
||
<span className="j13-deadline-countdown-unit">
|
||
<span className="j13-deadline-countdown-num tabular-nums">{pad2(r.days)}</span>
|
||
<span className="j13-deadline-countdown-suf">天</span>
|
||
</span>
|
||
<span className="j13-deadline-countdown-unit">
|
||
<span className="j13-deadline-countdown-num tabular-nums">{pad2(r.hours)}</span>
|
||
<span className="j13-deadline-countdown-suf">时</span>
|
||
</span>
|
||
<span className="j13-deadline-countdown-unit">
|
||
<span className="j13-deadline-countdown-num tabular-nums">{pad2(r.mins)}</span>
|
||
<span className="j13-deadline-countdown-suf">分</span>
|
||
</span>
|
||
<span className="j13-deadline-countdown-unit is-sec">
|
||
<span className="j13-deadline-countdown-num tabular-nums">{pad2(r.secs)}</span>
|
||
<span className="j13-deadline-countdown-suf">秒</span>
|
||
</span>
|
||
</div>
|
||
)}
|
||
<p className="j13-deadline-countdown-ends">截止于 {formatEndsAt(endsAt)}</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/** 正文下方的问答状态卡(未解决 / 仅标记已解决);有采纳答案时改用 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 (
|
||
<section className="j13-interact-card j13-typed-card" aria-label={`问答 · ${statusBadge.label}`}>
|
||
<div className="j13-typed-head">
|
||
<div className="j13-typed-head-main">
|
||
<h2 className="j13-typed-title is-question">
|
||
<CircleHelp size={16} strokeWidth={2.25} aria-hidden />
|
||
问答
|
||
</h2>
|
||
<span className={statusBadge.cls}>{statusBadge.label}</span>
|
||
</div>
|
||
{(state.can_solve || state.can_reopen) && (
|
||
<div className="j13-typed-actions">
|
||
{state.can_solve && (
|
||
<button
|
||
type="button"
|
||
className="btn btn-line btn-sm"
|
||
disabled={busy}
|
||
onClick={() => void act(() => apiSolveQuestion(String(postId)), "已标记为已解决")}
|
||
>
|
||
标记已解决
|
||
</button>
|
||
)}
|
||
{state.can_reopen && (
|
||
<button
|
||
type="button"
|
||
className="btn btn-line btn-sm"
|
||
disabled={busy}
|
||
onClick={() => void act(() => apiReopenQuestion(String(postId)), "已重新打开")}
|
||
>
|
||
重新打开
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<p className="j13-typed-meta">
|
||
{state.solved
|
||
? state.accepted_comment_id && allowComments
|
||
? (
|
||
<>
|
||
已采纳{" "}
|
||
<a
|
||
href={`#comment-${state.accepted_comment_id}`}
|
||
className="hover:underline"
|
||
style={{ color: "var(--accent)" }}
|
||
>
|
||
#{state.accepted_floor || "?"}
|
||
</a>
|
||
</>
|
||
)
|
||
: "作者已标记为已解决"
|
||
: allowComments
|
||
? "可在评论旁采纳答案,或手动标记已解决"
|
||
: "可由作者标记为已解决"}
|
||
</p>
|
||
|
||
<div className="j13-typed-foot">
|
||
<span className="j13-typed-foot-status">
|
||
<CheckCircle2 size={14} strokeWidth={2.25} aria-hidden />
|
||
{footLeft}
|
||
</span>
|
||
<TypedFootTime
|
||
ended={state.solved}
|
||
endedAt={state.solved_at || state.accepted_answer?.created_at}
|
||
/>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function PollPanel({
|
||
postId,
|
||
state,
|
||
loggedIn,
|
||
}: {
|
||
postId: number;
|
||
state: PollState;
|
||
loggedIn: boolean;
|
||
}) {
|
||
const router = useRouter();
|
||
const [sel, setSel] = useState<number[]>(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 (
|
||
<section className="j13-interact-card j13-typed-card" aria-label="投票">
|
||
<div className="j13-typed-head">
|
||
<div className="j13-typed-head-main">
|
||
<h2 className="j13-typed-title is-poll">
|
||
<BarChart3 size={16} strokeWidth={2.25} aria-hidden />
|
||
投票
|
||
</h2>
|
||
<span className={statusBadge.cls}>{statusBadge.label}</span>
|
||
</div>
|
||
{state.can_close && (
|
||
<div className="j13-typed-actions">
|
||
<button
|
||
type="button"
|
||
className="btn btn-line btn-sm"
|
||
disabled={busy}
|
||
onClick={() => void close()}
|
||
>
|
||
结束投票
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<p className="j13-typed-meta">
|
||
{state.multi ? "多选" : "单选"}
|
||
{state.anonymous ? " · 匿名" : " · 实名"}
|
||
{" · "}
|
||
{state.voters} 人参与 · {state.total_votes} 票
|
||
</p>
|
||
|
||
{!state.closed && state.ends_at ? (
|
||
<DeadlineCountdown
|
||
endsAt={state.ends_at}
|
||
expiredLabel="已到截止时间"
|
||
expiredHint="投票即将关闭"
|
||
/>
|
||
) : null}
|
||
|
||
<ul className="j13-poll-options">
|
||
{state.options.map((o) => {
|
||
const checked = sel.includes(o.index);
|
||
const namedVoters = !state.anonymous && showResults ? o.voters || [] : [];
|
||
return (
|
||
<li key={o.index}>
|
||
<button
|
||
type="button"
|
||
disabled={!state.can_vote || busy}
|
||
onClick={() => toggle(o.index)}
|
||
className={`j13-poll-option${checked ? " is-on" : ""}`}
|
||
>
|
||
{showResults && (
|
||
<span
|
||
className="j13-poll-option-bar"
|
||
style={{ width: `${o.percent}%` }}
|
||
aria-hidden
|
||
/>
|
||
)}
|
||
<span className="j13-poll-option-row">
|
||
<span className="j13-poll-option-text">
|
||
{state.can_vote ? (checked ? "✓ " : "") : ""}
|
||
{o.text}
|
||
</span>
|
||
{showResults && (
|
||
<span className="j13-poll-option-stat tabular-nums">
|
||
{o.votes} · {o.percent}%
|
||
</span>
|
||
)}
|
||
</span>
|
||
</button>
|
||
{namedVoters.length > 0 && (
|
||
<ul className="j13-poll-voters">
|
||
{namedVoters.map((v) => (
|
||
<li key={v.id}>
|
||
<Link href={`/u/${v.id}`} className="j13-poll-voter">
|
||
<Avatar name={v.nickname || v.username} src={v.avatar || undefined} size={16} />
|
||
{v.nickname || v.username}
|
||
</Link>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</li>
|
||
);
|
||
})}
|
||
</ul>
|
||
|
||
{(state.can_vote || (!loggedIn && !state.closed)) && (
|
||
<div className="j13-typed-actions-row">
|
||
{state.can_vote && (
|
||
<button
|
||
type="button"
|
||
className="btn btn-primary btn-sm"
|
||
disabled={busy}
|
||
onClick={() => void vote()}
|
||
>
|
||
提交投票
|
||
</button>
|
||
)}
|
||
{!loggedIn && !state.closed && (
|
||
<Link
|
||
href={`/login?redirect=${encodeURIComponent(`/post/${postId}`)}`}
|
||
className="btn btn-line btn-sm"
|
||
>
|
||
登录后投票
|
||
</Link>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
<div className="j13-typed-foot">
|
||
<span className="j13-typed-foot-status">
|
||
<CheckCircle2 size={14} strokeWidth={2.25} aria-hidden />
|
||
{footLeft}
|
||
</span>
|
||
<TypedFootTime
|
||
ended={state.closed}
|
||
endsAt={state.ends_at}
|
||
endedAt={state.closed_at}
|
||
/>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<section className="j13-interact-card j13-typed-card" aria-label="悬赏">
|
||
<div className="j13-typed-head">
|
||
<div className="j13-typed-head-main">
|
||
<h2 className="j13-typed-title is-bounty">
|
||
<Coins size={16} strokeWidth={2.25} aria-hidden />
|
||
悬赏
|
||
</h2>
|
||
<span className={statusBadge.cls}>{statusBadge.label}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<p className="j13-typed-meta">
|
||
<span className="j13-bounty-points tabular-nums">{state.points} 积分</span>
|
||
{bodyHint ? ` · ${bodyHint}` : null}
|
||
{state.can_accept && allowComments ? " · 在评论旁点击「采纳」发放积分" : null}
|
||
</p>
|
||
|
||
{!state.accepted_comment_id && state.ends_at ? (
|
||
<DeadlineCountdown
|
||
endsAt={state.ends_at}
|
||
expiredLabel="已到截止时间"
|
||
expiredHint={state.can_accept ? "已自动退回,仍可采纳并支付" : "等待处理"}
|
||
/>
|
||
) : null}
|
||
|
||
{allowComments && state.accepted_comment_id > 0 && (
|
||
<p className="j13-typed-meta">
|
||
已采纳{" "}
|
||
<a
|
||
href={`#comment-${state.accepted_comment_id}`}
|
||
className="hover:underline"
|
||
style={{ color: "var(--accent)" }}
|
||
>
|
||
#{state.accepted_floor || "?"}
|
||
</a>
|
||
</p>
|
||
)}
|
||
|
||
<div className="j13-typed-foot">
|
||
<span className="j13-typed-foot-status">
|
||
<CheckCircle2 size={14} strokeWidth={2.25} aria-hidden />
|
||
{footLeft}
|
||
</span>
|
||
<TypedFootTime
|
||
ended={!!state.accepted_comment_id || state.expired || state.refunded}
|
||
endsAt={state.ends_at}
|
||
endedAt={state.ends_at}
|
||
/>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<section className="j13-interact-card j13-typed-card" aria-label="抽奖">
|
||
<div className="j13-typed-head">
|
||
<div className="j13-typed-head-main">
|
||
<h2 className="j13-typed-title is-lottery">
|
||
<Gift size={16} strokeWidth={2.25} aria-hidden />
|
||
抽奖
|
||
</h2>
|
||
<span className={statusBadge.cls}>{statusBadge.label}</span>
|
||
</div>
|
||
{state.can_draw && (
|
||
<div className="j13-typed-actions">
|
||
<button
|
||
type="button"
|
||
className="btn btn-primary btn-sm"
|
||
disabled={busy}
|
||
onClick={() => void act(() => apiDrawLottery(String(postId)), "开奖完成")}
|
||
>
|
||
开奖
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{(state.can_draw || (state.eligible && !state.drawn)) && (
|
||
<p className="j13-typed-meta">
|
||
{state.can_draw ? `已有 ${state.entry_count} 名主评用户入池` : null}
|
||
{state.can_draw && state.eligible && !state.drawn ? " · " : null}
|
||
{state.eligible && !state.drawn ? "你已在奖池中" : null}
|
||
</p>
|
||
)}
|
||
|
||
{!state.drawn && state.ends_at ? (
|
||
<DeadlineCountdown
|
||
endsAt={state.ends_at}
|
||
expiredLabel="已到截止时间"
|
||
expiredHint="等待开奖"
|
||
/>
|
||
) : null}
|
||
|
||
{prizes.length > 0 && (
|
||
<ul className="j13-lottery-prize-row" aria-label="奖品">
|
||
{prizes.map((prize, i) => (
|
||
<li key={`${prize.name}-${i}`}>
|
||
<div className="j13-lottery-prize-chip" title={`${prize.name} ×${prize.count}`}>
|
||
<Trophy size={13} strokeWidth={2.25} aria-hidden />
|
||
<span className="j13-lottery-prize-chip-text">
|
||
{prize.name}
|
||
<span className="j13-lottery-prize-chip-count">×{prize.count}</span>
|
||
</span>
|
||
</div>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
|
||
{state.drawn && (
|
||
<>
|
||
<p className="j13-lottery-winners-label">中奖名单</p>
|
||
{state.winners.length > 0 ? (
|
||
<ul className="j13-lottery-winner-row">
|
||
{state.winners.map((w) => (
|
||
<li key={`${w.id}-${w.comment_id || 0}`}>
|
||
<div className="j13-lottery-winner">
|
||
<Avatar
|
||
name={w.nickname || w.username}
|
||
src={w.avatar || undefined}
|
||
size={36}
|
||
className="j13-lottery-winner-avatar"
|
||
/>
|
||
<div className="j13-lottery-winner-body">
|
||
<Link href={`/u/${w.id}`} className="j13-lottery-winner-name">
|
||
{w.nickname || w.username}
|
||
</Link>
|
||
{w.prize_name ? (
|
||
<span className="j13-lottery-winner-prize">{w.prize_name}</span>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
) : (
|
||
<p className="j13-typed-empty">无人回帖,开奖结果为空</p>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
<div className="j13-typed-foot">
|
||
<span className="j13-typed-foot-status">
|
||
<CheckCircle2 size={14} strokeWidth={2.25} aria-hidden />
|
||
{state.drawn ? "开奖完成" : "抽奖进行中"}
|
||
</span>
|
||
{state.ends_at ? (
|
||
<span className="j13-typed-foot-time">截止时间: {formatEndsAt(state.ends_at)}</span>
|
||
) : null}
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|