feat: 类型帖互动、积分经济与发帖体验

补齐问答采纳/重开、投票匿名与有票禁编、悬赏过期退回、抽奖回帖开奖,并接入积分账本与发帖附件可见性。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-16 03:17:07 +08:00
parent 1ef3d8e299
commit b9ba8cb8c9
58 changed files with 6913 additions and 506 deletions

View File

@@ -50,8 +50,16 @@ export function PulseCheckin({ initial }: { initial?: CheckinStatus | null }) {
{guest
? "登录后每日签到领积分"
: checked
? `连签 ${status?.streak ?? 0} 天 · 累计 ${status?.total_points ?? 0} 积分`
? `连签 ${status?.streak ?? 0} 天 · 余额 ${status?.total_points ?? 0}`
: `连签 ${status?.streak ?? 0} 天 · 今日 +${status?.today_points ?? 5}`}
{!guest && (
<>
{" · "}
<Link href="/points" className="hover:text-[var(--accent)] transition-colors">
流水
</Link>
</>
)}
</p>
</div>
{guest ? (

View File

@@ -67,7 +67,7 @@ function PulseCard({ stats, checkin }: { stats: OverviewStats; checkin?: Checkin
<span
className="inline-flex items-center gap-1.5 text-[11px] font-normal normal-case tracking-normal"
style={{ color: "var(--ink-3)" }}
title="近 5 分钟在线"
data-tip="近 5 分钟在线"
>
<span className="j13-pulse-dot" aria-hidden="true" />
实时
@@ -124,7 +124,7 @@ function Announcements({ items }: { items: AnnouncementSummary[] }) {
<span
className="mt-1.5 block truncate text-[13px] leading-snug transition-colors group-hover:text-[var(--accent)]"
style={{ color: "var(--ink)" }}
title={a.title}
data-tip={a.title}
>
{a.title}
</span>
@@ -151,7 +151,7 @@ function HotRanking({ posts }: { posts: PostListItem[] }) {
<ol className="j13-ranklist">
{posts.map((p, i) => (
<li key={p.id}>
<Link href={`/post/${p.id}`} className="j13-rank group" title={p.title}>
<Link href={`/post/${p.id}`} className="j13-rank group" data-tip={p.title}>
<span className="j13-rank-no">{i + 1}</span>
<span className="min-w-0 flex-1">
<span
@@ -200,7 +200,7 @@ function NewUsers({ users }: { users: NewUser[] }) {
<span
className="w-full truncate text-center text-[11px]"
style={{ color: "var(--ink-2)" }}
title={name}
data-tip={name}
>
{name}
</span>
@@ -232,7 +232,7 @@ function ActiveUsers({ users }: { users: ActiveUser[] }) {
const name = u.nickname || u.username;
return (
<li key={u.id}>
<Link href={`/u/${u.id}`} className="j13-raillink" title={name}>
<Link href={`/u/${u.id}`} className="j13-raillink" data-tip={name}>
<Avatar name={name} src={u.avatar} size={30} />
<span className="flex-1 min-w-0">
<span
@@ -294,7 +294,7 @@ function FriendLinks() {
{l.name}
</a>
) : (
<span key={l.name} className="j13-chip" title="友链待配置">
<span key={l.name} className="j13-chip" data-tip="友链待配置">
{l.name}
</span>
)

View File

@@ -14,7 +14,11 @@ function boardHref(id: number) {
export function WorkspaceNav({ boards, user }: { boards: BoardWithCount[]; user: User | null }) {
const params = useSearchParams();
const activeBoard = Number(params.get("board")) || 0;
const composeHref = user ? "/compose" : "/login?redirect=/compose";
const composePath =
activeBoard > 0 ? `/compose?board=${activeBoard}` : "/compose";
const composeHref = user
? composePath
: `/login?redirect=${encodeURIComponent(composePath)}`;
return (
<nav className="hidden lg:block w-56 xl:w-60 shrink-0" aria-label="工作台导航">
@@ -96,7 +100,11 @@ export function WorkspaceNavMobile({
}) {
const params = useSearchParams();
const activeBoard = Number(params.get("board")) || 0;
const composeHref = user ? "/compose" : "/login?redirect=/compose";
const composePath =
activeBoard > 0 ? `/compose?board=${activeBoard}` : "/compose";
const composeHref = user
? composePath
: `/login?redirect=${encodeURIComponent(composePath)}`;
return (
<div className="lg:hidden -mx-4 sm:-mx-6 px-4 sm:px-6">