feat: 类型帖互动、积分经济与发帖体验
补齐问答采纳/重开、投票匿名与有票禁编、悬赏过期退回、抽奖回帖开奖,并接入积分账本与发帖附件可见性。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -141,6 +141,9 @@ export interface PostListItem {
|
||||
title: string;
|
||||
tags: string;
|
||||
post_type: string;
|
||||
type_status?: string;
|
||||
content_access?: string;
|
||||
access_points?: number;
|
||||
pinned: number;
|
||||
recommended: boolean;
|
||||
like_count: number;
|
||||
@@ -157,6 +160,16 @@ export interface PostListItem {
|
||||
user: User;
|
||||
}
|
||||
|
||||
export interface PostAttachment {
|
||||
id: number;
|
||||
name: string;
|
||||
size: number;
|
||||
mime: string;
|
||||
price_points: number;
|
||||
download_count: number;
|
||||
unlocked: boolean;
|
||||
}
|
||||
|
||||
export interface Post {
|
||||
id: number;
|
||||
board_id: number;
|
||||
@@ -165,6 +178,10 @@ export interface Post {
|
||||
content: string;
|
||||
tags: string;
|
||||
post_type: string;
|
||||
content_access?: string;
|
||||
access_points?: number;
|
||||
type_meta?: string;
|
||||
type_status?: string;
|
||||
pinned: number;
|
||||
recommended: boolean;
|
||||
status: string;
|
||||
@@ -176,6 +193,80 @@ export interface Post {
|
||||
updated_at: string;
|
||||
board: Board;
|
||||
user: User;
|
||||
content_locked?: boolean;
|
||||
access_hint?: string;
|
||||
attachments?: PostAttachment[];
|
||||
question?: QuestionState | null;
|
||||
poll?: PollState | null;
|
||||
bounty?: BountyState | null;
|
||||
lottery?: LotteryState | null;
|
||||
}
|
||||
|
||||
export interface QuestionState {
|
||||
solved: boolean;
|
||||
accepted_comment_id: number;
|
||||
can_accept: boolean;
|
||||
can_solve: boolean;
|
||||
can_reopen: boolean;
|
||||
}
|
||||
|
||||
export interface PollVoter {
|
||||
id: number;
|
||||
nickname: string;
|
||||
username: string;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
export interface PollOptionStat {
|
||||
index: number;
|
||||
text: string;
|
||||
votes: number;
|
||||
percent: number;
|
||||
voters?: PollVoter[];
|
||||
}
|
||||
|
||||
export interface PollState {
|
||||
options: PollOptionStat[];
|
||||
multi: boolean;
|
||||
closed: boolean;
|
||||
anonymous: boolean;
|
||||
total_votes: number;
|
||||
voters: number;
|
||||
my_options: number[];
|
||||
can_vote: boolean;
|
||||
can_close: boolean;
|
||||
can_edit_options: boolean;
|
||||
}
|
||||
|
||||
export interface BountyState {
|
||||
points: number;
|
||||
accepted_comment_id: number;
|
||||
escrowed: boolean;
|
||||
refunded: boolean;
|
||||
expired?: boolean;
|
||||
ends_at?: string;
|
||||
settled: boolean;
|
||||
can_accept: boolean;
|
||||
can_refund: boolean;
|
||||
}
|
||||
|
||||
export interface LotteryEntrant {
|
||||
id: number;
|
||||
nickname: string;
|
||||
username: string;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
export interface LotteryState {
|
||||
slots: number;
|
||||
drawn: boolean;
|
||||
closed: boolean;
|
||||
ends_at?: string;
|
||||
entry_count: number;
|
||||
eligible?: boolean;
|
||||
can_draw: boolean;
|
||||
can_close: boolean;
|
||||
winners: LotteryEntrant[];
|
||||
}
|
||||
|
||||
export interface Comment {
|
||||
@@ -528,6 +619,10 @@ export async function apiCreatePost(data: {
|
||||
content: string;
|
||||
tags?: string;
|
||||
post_type?: string;
|
||||
content_access?: string;
|
||||
access_points?: number;
|
||||
type_meta?: string;
|
||||
attachment_ids?: number[];
|
||||
}) {
|
||||
const res = await fetchWithRefresh("/api/posts", {
|
||||
method: "POST",
|
||||
@@ -548,7 +643,15 @@ export async function apiCreateComment(postId: string, content: string, parentId
|
||||
|
||||
export async function apiUpdatePost(
|
||||
postId: string,
|
||||
data: { title?: string; content?: string; tags?: string }
|
||||
data: {
|
||||
title?: string;
|
||||
content?: string;
|
||||
tags?: string;
|
||||
content_access?: string;
|
||||
access_points?: number;
|
||||
type_meta?: string;
|
||||
attachment_ids?: number[];
|
||||
}
|
||||
) {
|
||||
const res = await fetchWithRefresh(`/api/posts/${postId}`, {
|
||||
method: "PUT",
|
||||
@@ -558,6 +661,181 @@ export async function apiUpdatePost(
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function apiUnlockPost(postId: string) {
|
||||
const res = await fetchWithRefresh(`/api/posts/${postId}/unlock`, {
|
||||
method: "POST",
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function apiGetPoints(): Promise<{ points: number }> {
|
||||
const res = await fetchWithRefresh("/api/points", {
|
||||
method: "GET",
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export interface PointLedgerItem {
|
||||
id: number;
|
||||
delta: number;
|
||||
balance: number;
|
||||
reason: string;
|
||||
ref_type: string;
|
||||
ref_id: number;
|
||||
note: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export async function apiGetPointsLedger(
|
||||
page = 1,
|
||||
size = 20
|
||||
): Promise<{ items: PointLedgerItem[]; total: number; page: number; size: number; error?: string }> {
|
||||
const res = await fetchWithRefresh(`/api/points/ledger?page=${page}&size=${size}`, {
|
||||
method: "GET",
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export interface EconomyStats {
|
||||
total_balance: number;
|
||||
ledger_count: number;
|
||||
checkin_today: number;
|
||||
open_bounties: number;
|
||||
escrowed_points: number;
|
||||
posts_by_type: Record<string, number>;
|
||||
recent_ledger: PointLedgerItem[];
|
||||
}
|
||||
|
||||
export async function apiAdminEconomyStats(): Promise<EconomyStats & { error?: string }> {
|
||||
const res = await fetchWithRefresh("/api/admin/economy", {
|
||||
method: "GET",
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function fetchAdminEconomyStats(
|
||||
cookieHeader?: string
|
||||
): Promise<EconomyStats> {
|
||||
const res = await fetch(`${API_BASE}/api/admin/economy`, ssrInit(cookieHeaders(cookieHeader)));
|
||||
if (!res.ok) throw new Error("failed");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function apiUploadPostFile(
|
||||
file: File,
|
||||
pricePoints = 0
|
||||
): Promise<{ attachment?: PostAttachment; error?: string }> {
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
fd.append("price_points", String(pricePoints));
|
||||
const res = await fetchWithRefresh("/api/upload/file", {
|
||||
method: "POST",
|
||||
headers: clientHeaders(),
|
||||
body: fd,
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function apiDeletePostFile(id: number) {
|
||||
const res = await fetchWithRefresh(`/api/upload/file/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function apiUpdatePostFilePrice(id: number, pricePoints: number) {
|
||||
const res = await fetchWithRefresh(`/api/upload/file/${id}/price`, {
|
||||
method: "PUT",
|
||||
headers: clientHeaders({ "Content-Type": "application/json" }),
|
||||
body: JSON.stringify({ price_points: pricePoints }),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export function postAttachmentDownloadURL(postId: number | string, attId: number) {
|
||||
return `/api/posts/${postId}/attachments/${attId}/download`;
|
||||
}
|
||||
|
||||
export async function apiVotePoll(postId: string, options: number[]) {
|
||||
const res = await fetchWithRefresh(`/api/posts/${postId}/poll/vote`, {
|
||||
method: "POST",
|
||||
headers: clientHeaders({ "Content-Type": "application/json" }),
|
||||
body: JSON.stringify({ options }),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function apiClosePoll(postId: string) {
|
||||
const res = await fetchWithRefresh(`/api/posts/${postId}/poll/close`, {
|
||||
method: "POST",
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function apiAcceptBounty(postId: string, commentId: number) {
|
||||
const res = await fetchWithRefresh(`/api/posts/${postId}/bounty/accept`, {
|
||||
method: "POST",
|
||||
headers: clientHeaders({ "Content-Type": "application/json" }),
|
||||
body: JSON.stringify({ comment_id: commentId }),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function apiRefundBounty(postId: string) {
|
||||
const res = await fetchWithRefresh(`/api/posts/${postId}/bounty/refund`, {
|
||||
method: "POST",
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function apiAcceptQuestion(postId: string, commentId: number) {
|
||||
const res = await fetchWithRefresh(`/api/posts/${postId}/question/accept`, {
|
||||
method: "POST",
|
||||
headers: clientHeaders({ "Content-Type": "application/json" }),
|
||||
body: JSON.stringify({ comment_id: commentId }),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function apiSolveQuestion(postId: string) {
|
||||
const res = await fetchWithRefresh(`/api/posts/${postId}/question/solve`, {
|
||||
method: "POST",
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function apiReopenQuestion(postId: string) {
|
||||
const res = await fetchWithRefresh(`/api/posts/${postId}/question/reopen`, {
|
||||
method: "POST",
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function apiCloseLottery(postId: string) {
|
||||
const res = await fetchWithRefresh(`/api/posts/${postId}/lottery/close`, {
|
||||
method: "POST",
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function apiDrawLottery(postId: string) {
|
||||
const res = await fetchWithRefresh(`/api/posts/${postId}/lottery/draw`, {
|
||||
method: "POST",
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function apiDeletePost(postId: string) {
|
||||
const res = await fetchWithRefresh(`/api/posts/${postId}`, {
|
||||
method: "DELETE",
|
||||
@@ -630,6 +908,25 @@ export async function apiUploadAvatar(
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// 上传帖子插图(JPEG / PNG / WebP,≤5MB)
|
||||
export async function apiUploadPostImage(
|
||||
file: File | Blob,
|
||||
filename = "image.jpg"
|
||||
): Promise<{ url?: string; attachment?: MediaAttachment; error?: string }> {
|
||||
const form = new FormData();
|
||||
const name =
|
||||
file instanceof File && file.name
|
||||
? file.name
|
||||
: filename;
|
||||
form.append("file", file, name);
|
||||
const res = await fetchWithRefresh("/api/upload/image", {
|
||||
method: "POST",
|
||||
headers: clientHeaders(),
|
||||
body: form,
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// 选用一张本人历史上传的头像
|
||||
export async function apiUseAvatar(
|
||||
url: string
|
||||
|
||||
Reference in New Issue
Block a user