first commit

This commit is contained in:
LeonspaceX
2025-10-18 17:34:11 +08:00
commit cb0fd04f59
43 changed files with 10922 additions and 0 deletions

732
src/admin_api.tsx Normal file
View File

@@ -0,0 +1,732 @@
import { API_CONFIG } from './config';
// 管理员认证相关的 API 接口
export interface AdminAuthResponse {
success: boolean;
message?: string;
}
// 管理员密码缓存键
const ADMIN_TOKEN_KEY = 'admin_token';
/**
* 获取存储的管理员令牌
*/
export const getAdminToken = (): string | null => {
return localStorage.getItem(ADMIN_TOKEN_KEY);
};
/**
* 存储管理员令牌
*/
export const setAdminToken = (token: string): void => {
localStorage.setItem(ADMIN_TOKEN_KEY, token);
};
/**
* 清除管理员令牌
*/
export const clearAdminToken = (): void => {
localStorage.removeItem(ADMIN_TOKEN_KEY);
};
/**
* 验证管理员密码
* @param password 管理员密码
* @returns Promise<AdminAuthResponse>
*/
export const verifyAdminPassword = async (password: string): Promise<AdminAuthResponse> => {
try {
const response = await fetch(`${API_CONFIG.BASE_URL}/admin/test`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${password}`,
'Content-Type': 'application/json',
},
});
if (response.status === 401 || response.status === 403) {
return {
success: false,
message: '密码错误,请重新输入'
};
}
if (response.ok) {
// 密码正确,存储到缓存
setAdminToken(password);
return {
success: true,
message: '登录成功'
};
}
// 其他错误状态
return {
success: false,
message: `服务器错误: ${response.status}`
};
} catch (error) {
console.error('Admin authentication error:', error);
return {
success: false,
message: '网络错误,请检查连接'
};
}
};
/**
* 检查管理员是否已登录
*/
export const isAdminLoggedIn = (): boolean => {
return getAdminToken() !== null;
};
/**
* 管理员退出登录
*/
export const adminLogout = (): void => {
clearAdminToken();
};
/**
* 创建带有管理员认证的请求头
*/
export const createAdminHeaders = (): HeadersInit => {
const token = getAdminToken();
return {
'Content-Type': 'application/json',
...(token && { 'Authorization': `Bearer ${token}` })
};
};
/**
* 通用的管理员 API 请求函数
* @param endpoint API 端点
* @param options 请求选项
*/
export const adminApiRequest = async (
endpoint: string,
options: RequestInit = {}
): Promise<Response> => {
const token = getAdminToken();
if (!token) {
throw new Error('未登录或登录已过期');
}
// 动态处理 Content-Type当 body 是 FormData 时让浏览器自动设置 boundary
const baseHeaders: HeadersInit = createAdminHeaders();
if (typeof FormData !== 'undefined' && options.body instanceof FormData) {
// @ts-ignore
delete baseHeaders['Content-Type'];
}
const response = await fetch(`${API_CONFIG.BASE_URL}/admin${endpoint}`, {
...options,
headers: {
...baseHeaders,
...options.headers,
},
});
// 如果返回 401 或 403说明令牌无效清除缓存
if (response.status === 401 || response.status === 403) {
clearAdminToken();
throw new Error('登录已过期,请重新登录');
}
return response;
};
/**
* 创建备份并返回 ZIP 文件 Blob 与文件名
* GET /admin/get/backup -> ZIP
*/
export const getBackupZip = async (): Promise<{ blob: Blob; filename: string }> => {
const resp = await adminApiRequest('/get/backup', { method: 'GET' });
if (!resp.ok) {
throw new Error(`创建备份失败: ${resp.status}`);
}
const disposition = resp.headers.get('Content-Disposition') || '';
let filename = 'backup.zip';
const match = disposition.match(/filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i);
if (match) {
filename = decodeURIComponent(match[1] || match[2] || filename);
}
const blob = await resp.blob();
return { blob, filename };
};
/**
* 恢复备份
* POST /admin/recover (multipart/form-data: backup_file)
*/
export const recoverBackup = async (file: File): Promise<{ status: 'OK' }> => {
const form = new FormData();
// 后端要求的字段名file
form.append('file', file);
const resp = await adminApiRequest('/recover', {
method: 'POST',
body: form,
// 让 adminApiRequest 自动去掉 Content-Type
});
if (!resp.ok) {
let detail = '';
try {
const ct = resp.headers.get('Content-Type') || '';
if (ct.includes('application/json')) {
const data = await resp.json();
detail = typeof data === 'string' ? data : (data?.message || JSON.stringify(data));
} else {
detail = await resp.text();
}
} catch {}
throw new Error(`恢复备份失败: ${resp.status}${detail ? ` - ${detail}` : ''}`);
}
return resp.json();
};
/**
* 获取当前帖子审核模式
* GET /admin/get/need_audit -> { status: boolean }
*/
export const getAuditMode = async (): Promise<{ status: boolean }> => {
const resp = await adminApiRequest('/get/need_audit', {
method: 'GET',
});
if (!resp.ok) {
throw new Error(`获取审核模式失败: ${resp.status}`);
}
return resp.json();
};
/**
* 切换帖子审核模式
* POST /admin/need_audit { need_audit: boolean }
*/
export const setAuditMode = async (need_audit: boolean): Promise<{ status: 'OK' }> => {
const resp = await adminApiRequest('/need_audit', {
method: 'POST',
body: JSON.stringify({ need_audit }),
});
if (!resp.ok) {
throw new Error(`切换审核模式失败: ${resp.status}`);
}
return resp.json();
};
/**
* 图片链接项
*/
export interface PicLink {
filename: string;
url: string;
upload_time: string;
}
/**
* 获取图片链接列表
* GET /admin/get/pic_links?page=1 -> PicLink[]
*/
export const getPicLinks = async (page: number = 1): Promise<PicLink[]> => {
const resp = await adminApiRequest(`/get/pic_links?page=${encodeURIComponent(page)}`, {
method: 'GET',
});
if (!resp.ok) {
throw new Error(`获取图片链接失败: ${resp.status}`);
}
const data = await resp.json();
// 兼容字符串数组(如 ["/img/251012_xxx.png", ...])与对象数组的返回
return (Array.isArray(data) ? data : []).map((item: any) => {
// 字符串项:直接视为图片相对或绝对 URL
if (typeof item === 'string') {
const raw = item.trim();
const isAbsolute = /^https?:\/\//i.test(raw);
const path = isAbsolute ? raw : (raw.startsWith('/') ? raw : `/${raw}`);
const url = isAbsolute ? raw : `${API_CONFIG.BASE_URL}${path}`;
// 从路径派生 filename
let filename = '';
if (raw.startsWith('/img/')) {
filename = raw.slice('/img/'.length);
} else {
const idx = raw.lastIndexOf('/');
filename = idx >= 0 ? raw.slice(idx + 1) : raw;
}
try { filename = decodeURIComponent(filename); } catch {}
return { filename, url, upload_time: '' } as PicLink;
}
// 对象项:使用字段并进行回退与绝对化
const filename = String(item?.filename || '');
const upload_time = String(item?.upload_time || '');
const urlRaw = item?.url;
let url = typeof urlRaw === 'string' ? urlRaw.trim() : '';
if (!url && filename) {
url = `/img/${encodeURIComponent(filename)}`;
}
if (url && !/^https?:\/\//i.test(url)) {
const path = url.startsWith('/') ? url : `/${url}`;
url = `${API_CONFIG.BASE_URL}${path}`;
}
return { filename, url, upload_time } as PicLink;
});
};
/**
* 删除图片
* POST /admin/del_pic { filename }
*/
export const deletePic = async (filename: string): Promise<{ status: 'OK' }> => {
if (!filename) {
throw new Error('缺少图片文件名');
}
const resp = await adminApiRequest('/del_pic', {
method: 'POST',
body: JSON.stringify({ filename }),
});
if (!resp.ok) {
let detail = '';
try {
const ct = resp.headers.get('Content-Type') || '';
if (ct.includes('application/json')) {
const data = await resp.json();
detail = typeof data === 'string' ? data : (data?.message || JSON.stringify(data));
} else {
detail = await resp.text();
}
} catch {}
throw new Error(`删除图片失败: ${resp.status}${detail ? ` - ${detail}` : ''}`);
}
return resp.json();
};
/**
* 待处理举报项
*/
export interface PendingReport {
id: number;
submission_id: number;
title: string;
content: string;
status: string;
created_at: string;
}
/**
* 获取待处理举报列表
* GET /admin/get/pending_reports -> PendingReport[]
*/
export const getPendingReports = async (): Promise<PendingReport[]> => {
const resp = await adminApiRequest('/get/pending_reports', { method: 'GET' });
if (!resp.ok) {
throw new Error(`获取待处理举报失败: ${resp.status}`);
}
const data = await resp.json();
return (Array.isArray(data) ? data : []).map((item: any) => ({
id: Number(item?.id ?? 0),
submission_id: Number(item?.submission_id ?? 0),
title: String(item?.title ?? ''),
content: String(item?.content ?? ''),
status: String(item?.status ?? ''),
created_at: String(item?.created_at ?? ''),
}));
};
/**
* 批准举报
* POST /admin/approve_report { id }
*/
export const approveReport = async (id: number): Promise<{ status: 'OK' }> => {
if (!id && id !== 0) {
throw new Error('缺少举报 ID');
}
const resp = await adminApiRequest('/approve_report', {
method: 'POST',
body: JSON.stringify({ id }),
});
if (!resp.ok) {
let detail = '';
try {
const ct = resp.headers.get('Content-Type') || '';
if (ct.includes('application/json')) {
const data = await resp.json();
detail = typeof data === 'string' ? data : (data?.message || JSON.stringify(data));
} else {
detail = await resp.text();
}
} catch {}
throw new Error(`批准举报失败: ${resp.status}${detail ? ` - ${detail}` : ''}`);
}
return resp.json();
};
/**
* 拒绝举报
* POST /admin/reject_report { id }
*/
export const rejectReport = async (id: number): Promise<{ status: 'OK' }> => {
if (!id && id !== 0) {
throw new Error('缺少举报 ID');
}
const resp = await adminApiRequest('/reject_report', {
method: 'POST',
body: JSON.stringify({ id }),
});
if (!resp.ok) {
let detail = '';
try {
const ct = resp.headers.get('Content-Type') || '';
if (ct.includes('application/json')) {
const data = await resp.json();
detail = typeof data === 'string' ? data : (data?.message || JSON.stringify(data));
} else {
detail = await resp.text();
}
} catch {}
throw new Error(`拒绝举报失败: ${resp.status}${detail ? ` - ${detail}` : ''}`);
}
return resp.json();
};
/**
* 获取管理员视角的帖子详情(只需 content 字段)
* GET /admin/get/post_info?id=number -> { content: string, ... }
*/
export interface AdminPostInfo { content: string }
export const getAdminPostInfo = async (id: number): Promise<AdminPostInfo> => {
if (!id && id !== 0) {
throw new Error('缺少帖子 ID');
}
const resp = await adminApiRequest(`/get/post_info?id=${encodeURIComponent(id)}`, { method: 'GET' });
if (!resp.ok) {
let detail = '';
try {
const ct = resp.headers.get('Content-Type') || '';
if (ct.includes('application/json')) {
const data = await resp.json();
detail = typeof data === 'string' ? data : (data?.message || JSON.stringify(data));
} else {
detail = await resp.text();
}
} catch {}
throw new Error(`获取帖子详情失败: ${resp.status}${detail ? ` - ${detail}` : ''}`);
}
const data = await resp.json();
return { content: String(data?.content || '') };
};
/**
* 管理端帖子列表项(用于待审核/已拒绝)
*/
export interface AdminPostListItem {
id: number;
content: string;
create_time: string;
upvotes: number;
downvotes: number;
}
/**
* 获取待审核帖子列表
* GET /admin/get/pending_posts -> AdminPostListItem[]
*/
export const getPendingPosts = async (): Promise<AdminPostListItem[]> => {
const resp = await adminApiRequest('/get/pending_posts', { method: 'GET' });
if (!resp.ok) {
let detail = '';
try {
const ct = resp.headers.get('Content-Type') || '';
if (ct.includes('application/json')) {
const data = await resp.json();
detail = typeof data === 'string' ? data : (data?.message || JSON.stringify(data));
} else {
detail = await resp.text();
}
} catch {}
throw new Error(`获取待审核帖子失败: ${resp.status}${detail ? ` - ${detail}` : ''}`);
}
const data = await resp.json();
return Array.isArray(data) ? data as AdminPostListItem[] : [];
};
/**
* 获取已拒绝帖子列表
* GET /admin/get/reject_posts -> AdminPostListItem[]
*/
export const getRejectedPosts = async (): Promise<AdminPostListItem[]> => {
const resp = await adminApiRequest('/get/reject_posts', { method: 'GET' });
if (!resp.ok) {
let detail = '';
try {
const ct = resp.headers.get('Content-Type') || '';
if (ct.includes('application/json')) {
const data = await resp.json();
detail = typeof data === 'string' ? data : (data?.message || JSON.stringify(data));
} else {
detail = await resp.text();
}
} catch {}
throw new Error(`获取已拒绝帖子失败: ${resp.status}${detail ? ` - ${detail}` : ''}`);
}
const data = await resp.json();
return Array.isArray(data) ? data as AdminPostListItem[] : [];
};
/**
* 审核通过帖子
* POST /admin/approve { id }
*/
export const approvePost = async (id: number): Promise<{ status: 'OK' }> => {
if (!id && id !== 0) {
throw new Error('缺少帖子 ID');
}
const resp = await adminApiRequest('/approve', {
method: 'POST',
body: JSON.stringify({ id }),
});
if (!resp.ok) {
let detail = '';
try {
const ct = resp.headers.get('Content-Type') || '';
if (ct.includes('application/json')) {
const data = await resp.json();
detail = typeof data === 'string' ? data : (data?.message || JSON.stringify(data));
} else {
detail = await resp.text();
}
} catch {}
const msg = resp.status === 401 || resp.status === 403
? '身份验证失败,请重新登陆'
: resp.status === 404
? '帖子不存在'
: resp.status === 400
? '缺少帖子 ID'
: `审核通过失败: ${resp.status}${detail ? ` - ${detail}` : ''}`;
throw new Error(msg);
}
return resp.json();
};
/**
* 拒绝帖子
* POST /admin/disapprove { id }
*/
export const disapprovePost = async (id: number): Promise<{ status: 'OK' }> => {
if (!id && id !== 0) {
throw new Error('缺少帖子 ID');
}
const resp = await adminApiRequest('/disapprove', {
method: 'POST',
body: JSON.stringify({ id }),
});
if (!resp.ok) {
let detail = '';
try {
const ct = resp.headers.get('Content-Type') || '';
if (ct.includes('application/json')) {
const data = await resp.json();
detail = typeof data === 'string' ? data : (data?.message || JSON.stringify(data));
} else {
detail = await resp.text();
}
} catch {}
const msg = resp.status === 401 || resp.status === 403
? '身份验证失败,请重新登陆'
: resp.status === 404
? '帖子不存在'
: resp.status === 400
? '缺少帖子 ID'
: `拒绝帖子失败: ${resp.status}${detail ? ` - ${detail}` : ''}`;
throw new Error(msg);
}
return resp.json();
};
/**
* 重新审核帖子(将已通过设回待审核)
* POST /admin/reaudit { id }
*/
export const reauditPost = async (id: number): Promise<{ status: 'OK' }> => {
if (!id && id !== 0) {
throw new Error('缺少帖子 ID');
}
const resp = await adminApiRequest('/reaudit', {
method: 'POST',
body: JSON.stringify({ id }),
});
if (!resp.ok) {
let detail = '';
try {
const ct = resp.headers.get('Content-Type') || '';
if (ct.includes('application/json')) {
const data = await resp.json();
detail = typeof data === 'string' ? data : (data?.message || JSON.stringify(data));
} else {
detail = await resp.text();
}
} catch {}
const msg = resp.status === 401 || resp.status === 403
? '身份验证失败,请重新登陆'
: resp.status === 404
? '帖子不存在'
: resp.status === 400
? '缺少帖子 ID'
: `重新审核失败: ${resp.status}${detail ? ` - ${detail}` : ''}`;
throw new Error(msg);
}
return resp.json();
};
/**
* 删除帖子
* POST /admin/del_post { id }
*/
export const deletePost = async (id: number): Promise<{ status: 'OK' }> => {
if (!id && id !== 0) {
throw new Error('缺少帖子 ID');
}
const resp = await adminApiRequest('/del_post', {
method: 'POST',
body: JSON.stringify({ id }),
});
if (!resp.ok) {
let detail = '';
try {
const ct = resp.headers.get('Content-Type') || '';
if (ct.includes('application/json')) {
const data = await resp.json();
detail = typeof data === 'string' ? data : (data?.message || JSON.stringify(data));
} else {
detail = await resp.text();
}
} catch {}
const msg = resp.status === 401 || resp.status === 403
? '身份验证失败,请重新登陆'
: resp.status === 404
? '帖子不存在'
: resp.status === 400
? '缺少帖子 ID'
: `删除帖子失败: ${resp.status}${detail ? ` - ${detail}` : ''}`;
throw new Error(msg);
}
return resp.json();
};
/**
* 修改帖子内容
* POST /admin/modify_post { id, content }
*/
export const modifyPost = async (
id: number,
content: string
): Promise<{ status: 'OK' }> => {
if ((!id && id !== 0) || !content) {
throw new Error(!content ? '缺少帖子内容' : '缺少帖子 ID');
}
const resp = await adminApiRequest('/modify_post', {
method: 'POST',
body: JSON.stringify({ id, content }),
});
if (!resp.ok) {
let detail = '';
try {
const ct = resp.headers.get('Content-Type') || '';
if (ct.includes('application/json')) {
const data = await resp.json();
detail = typeof data === 'string' ? data : (data?.message || JSON.stringify(data));
} else {
detail = await resp.text();
}
} catch {}
const msg = resp.status === 401 || resp.status === 403
? '身份验证失败,请重新登陆'
: resp.status === 404
? '帖子不存在'
: resp.status === 400
? '缺少 ID 或 content'
: `修改帖子失败: ${resp.status}${detail ? ` - ${detail}` : ''}`;
throw new Error(msg);
}
return resp.json();
};
/**
* 删除评论
* POST /admin/del_comment { id }
*/
export const deleteComment = async (id: number): Promise<{ status: 'OK' }> => {
if (!id && id !== 0) {
throw new Error('缺少评论 ID');
}
const resp = await adminApiRequest('/del_comment', {
method: 'POST',
body: JSON.stringify({ id }),
});
if (!resp.ok) {
let detail = '';
try {
const ct = resp.headers.get('Content-Type') || '';
if (ct.includes('application/json')) {
const data = await resp.json();
detail = typeof data === 'string' ? data : (data?.message || JSON.stringify(data));
} else {
detail = await resp.text();
}
} catch {}
const msg = resp.status === 401 || resp.status === 403
? '身份验证失败,请重新登陆'
: resp.status === 404
? '评论不存在'
: resp.status === 400
? '缺少评论 ID'
: `删除评论失败: ${resp.status}${detail ? ` - ${detail}` : ''}`;
throw new Error(msg);
}
return resp.json();
};
/**
* 修改评论
* POST /admin/modify_comment { id, content, parent_comment_id, nickname }
*/
export const modifyComment = async (
id: number,
content: string,
parent_comment_id: number,
nickname: string
): Promise<{ status: 'OK' }> => {
const missingId = !id && id !== 0;
const missingParent = parent_comment_id === undefined || parent_comment_id === null || Number.isNaN(parent_comment_id);
if (missingId || !content || !nickname || missingParent) {
throw new Error('缺少必填字段');
}
const resp = await adminApiRequest('/modify_comment', {
method: 'POST',
body: JSON.stringify({ id, content, parent_comment_id, nickname }),
});
if (!resp.ok) {
let detail = '';
try {
const ct = resp.headers.get('Content-Type') || '';
if (ct.includes('application/json')) {
const data = await resp.json();
detail = typeof data === 'string' ? data : (data?.message || JSON.stringify(data));
} else {
detail = await resp.text();
}
} catch {}
const msg = resp.status === 401 || resp.status === 403
? '身份验证失败,请重新登陆'
: resp.status === 404
? '评论或父评论不存在'
: resp.status === 400
? '缺少必填字段'
: `修改评论失败: ${resp.status}${detail ? ` - ${detail}` : ''}`;
throw new Error(msg);
}
return resp.json();
};