feat: 管理端板块管理与旧版数据导入,补充部署运营文档

- 新增管理端板块管理页面与后端接口(admin_board)
- 新增旧版数据导入:legacyimport 服务、导入面板、importusers 命令行工具
- 聊天用户卡片、板块图标等 UI 组件与界面优化
- 补充 about/公告/1Panel 部署等文档
- gitignore 排除 dist/ 构建产物与 .agents/ 本地工具目录
This commit is contained in:
2026-09-24 03:37:44 +08:00
parent c1f6a6636b
commit 3e55b5d230
53 changed files with 5078 additions and 409 deletions

View File

@@ -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

View File

@@ -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;