feat: 管理端板块管理与旧版数据导入,补充部署运营文档
- 新增管理端板块管理页面与后端接口(admin_board) - 新增旧版数据导入:legacyimport 服务、导入面板、importusers 命令行工具 - 聊天用户卡片、板块图标等 UI 组件与界面优化 - 补充 about/公告/1Panel 部署等文档 - gitignore 排除 dist/ 构建产物与 .agents/ 本地工具目录
This commit is contained in:
@@ -82,8 +82,47 @@ export interface Board {
|
||||
icon: string;
|
||||
color_index?: number;
|
||||
sort_order: number;
|
||||
post_policy?: string;
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
// 管理端板块:列表附带已发布帖子数
|
||||
export interface AdminBoard {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
color_index: number;
|
||||
sort_order: number;
|
||||
post_policy: string;
|
||||
visible: boolean;
|
||||
post_count?: number;
|
||||
}
|
||||
|
||||
export interface AdminBoardModerator {
|
||||
id: number;
|
||||
username: string;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
email: string;
|
||||
banned: boolean;
|
||||
}
|
||||
|
||||
export type AdminBoardInput = {
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
color_index?: number | null;
|
||||
sort_order?: number | null;
|
||||
post_policy?: string;
|
||||
visible?: boolean | null;
|
||||
};
|
||||
|
||||
export const BOARD_POST_POLICY = {
|
||||
ALL: "all",
|
||||
STAFF: "staff",
|
||||
} as const;
|
||||
|
||||
// 首页聚合接口
|
||||
export interface OverviewStats {
|
||||
posts: number;
|
||||
@@ -695,10 +734,12 @@ export async function fetchBoards(): Promise<{ boards: BoardWithCount[] }> {
|
||||
}
|
||||
|
||||
// 公开站点设置:accent 为站点主题色 hex(空串=内置默认);
|
||||
// trust_reviewed_publish 为已过审用户后续免审(缺省/未配置视为 true)
|
||||
// trust_reviewed_publish 为已过审用户后续免审(缺省/未配置视为 true);
|
||||
// publish_without_review 为全站免审,无需过审记录直接发布(缺省/未配置视为 false)
|
||||
export interface PublicSettings {
|
||||
accent: string;
|
||||
trust_reviewed_publish: boolean;
|
||||
publish_without_review: boolean;
|
||||
site_name: string;
|
||||
site_description: string;
|
||||
/** 无 Logo 时页眉字标;空则回退显示名称 */
|
||||
@@ -772,6 +813,7 @@ export const DEFAULT_ATTACHMENT_EXTS: string[] = [
|
||||
const DEFAULT_PUBLIC_SETTINGS: PublicSettings = {
|
||||
accent: "",
|
||||
trust_reviewed_publish: true,
|
||||
publish_without_review: false,
|
||||
site_name: "姜十三论坛",
|
||||
site_description: "姜十三论坛 - 技术分享与讨论社区",
|
||||
site_wordmark: "",
|
||||
@@ -842,6 +884,7 @@ function normalizePublicSettings(data: Partial<PublicSettings> | null | undefine
|
||||
return {
|
||||
accent: data?.accent ?? "",
|
||||
trust_reviewed_publish: data?.trust_reviewed_publish !== false,
|
||||
publish_without_review: data?.publish_without_review === true,
|
||||
site_name: (data?.site_name || "").trim() || DEFAULT_PUBLIC_SETTINGS.site_name,
|
||||
site_description:
|
||||
data?.site_description !== undefined
|
||||
@@ -2142,6 +2185,187 @@ export async function apiAdminSetUserBan(
|
||||
return data as { user: AdminUser };
|
||||
}
|
||||
|
||||
// ===== 数据导入(超管/站长) =====
|
||||
|
||||
export type LegacyUserReport = {
|
||||
total: number;
|
||||
imported: number;
|
||||
skipped: number;
|
||||
avatar_written: number;
|
||||
conflicts?: string[];
|
||||
avatar_missing?: string[];
|
||||
failed?: string[];
|
||||
};
|
||||
|
||||
export type LegacyBoardReport = {
|
||||
created: number;
|
||||
reused: number;
|
||||
names?: string[];
|
||||
};
|
||||
|
||||
export type LegacyPostReport = {
|
||||
total: number;
|
||||
imported: number;
|
||||
skipped: number;
|
||||
polls_skipped: number;
|
||||
poll_titles?: string[];
|
||||
owner_fallback?: string[];
|
||||
failed?: string[];
|
||||
};
|
||||
|
||||
export type LegacyCommentReport = {
|
||||
total: number;
|
||||
imported: number;
|
||||
skipped: number;
|
||||
skipped_detail?: string[];
|
||||
owner_fallback?: string[];
|
||||
failed?: string[];
|
||||
};
|
||||
|
||||
export type LegacyImportReport = {
|
||||
dry_run: boolean;
|
||||
users: LegacyUserReport;
|
||||
boards?: LegacyBoardReport;
|
||||
posts?: LegacyPostReport;
|
||||
comments?: LegacyCommentReport;
|
||||
notes?: string[];
|
||||
};
|
||||
|
||||
export type LegacyImportOptions = {
|
||||
withContent: boolean;
|
||||
dryRun: boolean;
|
||||
};
|
||||
|
||||
/** 上传旧站 SQLite 库(必填)与可选头像 zip,导入用户与内容。幂等可重复执行 */
|
||||
export async function apiAdminImportLegacy(
|
||||
source: string,
|
||||
dbFile: File,
|
||||
zipFile: File | null,
|
||||
opts: LegacyImportOptions
|
||||
): Promise<LegacyImportReport> {
|
||||
const form = new FormData();
|
||||
form.append("db_file", dbFile, dbFile.name || "jiang13.db");
|
||||
if (zipFile) form.append("avatars_zip", zipFile, zipFile.name || "avatars.zip");
|
||||
form.append("with_content", String(opts.withContent));
|
||||
form.append("dry_run", String(opts.dryRun));
|
||||
const res = await fetchWithRefresh(`/api/admin/import/${source}`, {
|
||||
method: "POST",
|
||||
headers: clientHeaders(),
|
||||
body: form,
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "导入失败");
|
||||
return (data.report || {}) as LegacyImportReport;
|
||||
}
|
||||
|
||||
// ===== 板块管理(仅站长) =====
|
||||
|
||||
export async function fetchAdminBoards(
|
||||
cookieHeader?: string
|
||||
): Promise<{ boards: AdminBoard[] }> {
|
||||
const res = await fetch(
|
||||
`${API_BASE}/api/admin/boards`,
|
||||
ssrInit(cookieHeaders(cookieHeader))
|
||||
);
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "获取板块列表失败");
|
||||
return data as { boards: AdminBoard[] };
|
||||
}
|
||||
|
||||
export async function apiAdminListBoards(): Promise<{ boards: AdminBoard[] }> {
|
||||
const res = await fetchWithRefresh("/api/admin/boards", {
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "获取板块列表失败");
|
||||
return data as { boards: AdminBoard[] };
|
||||
}
|
||||
|
||||
export async function apiAdminCreateBoard(
|
||||
input: AdminBoardInput
|
||||
): Promise<{ board: AdminBoard }> {
|
||||
const res = await fetchWithRefresh("/api/admin/boards", {
|
||||
method: "POST",
|
||||
headers: clientHeaders({ "Content-Type": "application/json" }),
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "创建板块失败");
|
||||
return data as { board: AdminBoard };
|
||||
}
|
||||
|
||||
export async function apiAdminUpdateBoard(
|
||||
id: number,
|
||||
input: AdminBoardInput
|
||||
): Promise<{ board: AdminBoard }> {
|
||||
const res = await fetchWithRefresh(`/api/admin/boards/${id}`, {
|
||||
method: "PUT",
|
||||
headers: clientHeaders({ "Content-Type": "application/json" }),
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "更新板块失败");
|
||||
return data as { board: AdminBoard };
|
||||
}
|
||||
|
||||
export async function apiAdminDeleteBoard(id: number): Promise<void> {
|
||||
const res = await fetchWithRefresh(`/api/admin/boards/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "删除板块失败");
|
||||
}
|
||||
|
||||
export async function apiAdminReorderBoards(ids: number[]): Promise<void> {
|
||||
const res = await fetchWithRefresh("/api/admin/boards/reorder", {
|
||||
method: "PUT",
|
||||
headers: clientHeaders({ "Content-Type": "application/json" }),
|
||||
body: JSON.stringify({ ids }),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "排序失败");
|
||||
}
|
||||
|
||||
export async function apiAdminListBoardModerators(
|
||||
id: number
|
||||
): Promise<{ moderators: AdminBoardModerator[] }> {
|
||||
const res = await fetchWithRefresh(`/api/admin/boards/${id}/moderators`, {
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "获取版主失败");
|
||||
return data as { moderators: AdminBoardModerator[] };
|
||||
}
|
||||
|
||||
export async function apiAdminAddBoardModerator(
|
||||
boardId: number,
|
||||
username: string
|
||||
): Promise<void> {
|
||||
const res = await fetchWithRefresh(`/api/admin/boards/${boardId}/moderators`, {
|
||||
method: "POST",
|
||||
headers: clientHeaders({ "Content-Type": "application/json" }),
|
||||
body: JSON.stringify({ username }),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "添加版主失败");
|
||||
}
|
||||
|
||||
export async function apiAdminRemoveBoardModerator(
|
||||
boardId: number,
|
||||
uid: number
|
||||
): Promise<void> {
|
||||
const res = await fetchWithRefresh(
|
||||
`/api/admin/boards/${boardId}/moderators/${uid}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: clientHeaders(),
|
||||
}
|
||||
);
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "移除版主失败");
|
||||
}
|
||||
|
||||
// ===== 站长用户内容档案 =====
|
||||
|
||||
export interface AdminUserAuditProfile {
|
||||
@@ -2277,6 +2501,7 @@ export async function apiGetSettings(): Promise<PublicSettings> {
|
||||
export type UpdateSiteSettingsBody = {
|
||||
accent?: string;
|
||||
trust_reviewed_publish?: boolean;
|
||||
publish_without_review?: boolean;
|
||||
site_name?: string;
|
||||
site_description?: string;
|
||||
site_wordmark?: string;
|
||||
@@ -2790,6 +3015,16 @@ export async function apiOpenDirectChat(
|
||||
return data as { room: ChatRoomView };
|
||||
}
|
||||
|
||||
// 客户端:公开用户资料(资料卡用;只取第一页减小体积)
|
||||
export async function apiGetUserProfile(id: number): Promise<UserProfile> {
|
||||
const res = await fetchWithRefresh(`/api/users/${id}?page=1&size=1`, {
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "获取用户资料失败");
|
||||
return data as UserProfile;
|
||||
}
|
||||
|
||||
// 客户端:搜索用户以发起私聊
|
||||
export async function apiSearchChatUsers(
|
||||
q: string
|
||||
|
||||
@@ -41,6 +41,7 @@ export interface RtPresenceData {
|
||||
export interface RtSettingsData {
|
||||
accent: string;
|
||||
trust_reviewed_publish?: boolean;
|
||||
publish_without_review?: boolean;
|
||||
site_name?: string;
|
||||
site_description?: string;
|
||||
site_wordmark?: string;
|
||||
|
||||
Reference in New Issue
Block a user