feat: 完成头像上传、站点主题配置与UI优化
- 新增附件模型与上传服务,拆分头像上传/管理逻辑 - 重构用户资料接口,移除直接修改头像字段,改用专用上传接口 - 添加站点公开设置API与管理员外观配置页 - 全局替换hover背景为accent-soft,优化交互一致性 - 完善头像组件,支持图片加载失败回退与src更新重置 - 新增用户状态事件总线,解决同页头像更新不及时问题 - 调整依赖包顺序与CI依赖版本 - 重构板块列表接口,新增帖子计数 - 迁移账号设置到用户主页tab,合并重复代码
This commit is contained in:
@@ -349,12 +349,28 @@ export async function fetchOverview(cookieHeader?: string): Promise<OverviewResp
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function fetchBoards(): Promise<{ boards: Board[] }> {
|
||||
export async function fetchBoards(): Promise<{ boards: BoardWithCount[] }> {
|
||||
const res = await fetch(`${API_BASE}/api/boards`, ssrInit());
|
||||
if (!res.ok) throw new Error("获取板块失败");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// 公开站点设置:accent 为站点主题色 hex(空串=内置默认)
|
||||
export interface PublicSettings {
|
||||
accent: string;
|
||||
}
|
||||
|
||||
// SSR 读取公开站点设置(主题色注入用);后端不可用时降级为默认色,不阻塞渲染
|
||||
export async function fetchPublicSettings(): Promise<PublicSettings> {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/settings`, ssrInit());
|
||||
if (!res.ok) return { accent: "" };
|
||||
return (await res.json()) as PublicSettings;
|
||||
} catch {
|
||||
return { accent: "" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchPostDetail(id: string, cookieHeader?: string): Promise<{ post: Post }> {
|
||||
const res = await fetch(`${API_BASE}/api/posts/${id}`, ssrInit(cookieHeaders(cookieHeader)));
|
||||
if (!res.ok) throw new Error("获取帖子失败");
|
||||
@@ -517,7 +533,6 @@ export async function apiChangePassword(oldPassword: string, newPassword: string
|
||||
// 更新当前用户资料(昵称、头像、邮箱、签名)
|
||||
export async function apiUpdateProfile(data: {
|
||||
nickname: string;
|
||||
avatar?: string;
|
||||
email?: string;
|
||||
signature?: string;
|
||||
}): Promise<{ user?: User; error?: string }> {
|
||||
@@ -529,6 +544,66 @@ export async function apiUpdateProfile(data: {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// 媒体库附件(avatar = 头像,image = 帖子插图等)
|
||||
export interface MediaAttachment {
|
||||
id: number;
|
||||
url: string;
|
||||
kind: "avatar" | "image";
|
||||
size: number;
|
||||
width: number;
|
||||
height: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// 上传裁剪后的头像(Blob 必须是 WebP;不要手动设置 Content-Type,交给浏览器加 multipart 边界)
|
||||
export async function apiUploadAvatar(
|
||||
file: Blob
|
||||
): Promise<{ url?: string; error?: string }> {
|
||||
const form = new FormData();
|
||||
form.append("file", file, "avatar.webp");
|
||||
const res = await fetchWithRefresh("/api/upload/avatar", {
|
||||
method: "POST",
|
||||
headers: clientHeaders(),
|
||||
body: form,
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// 选用一张本人历史上传的头像
|
||||
export async function apiUseAvatar(
|
||||
url: string
|
||||
): Promise<{ url?: string; error?: string }> {
|
||||
const res = await fetchWithRefresh("/api/avatar/use", {
|
||||
method: "PUT",
|
||||
headers: clientHeaders({ "Content-Type": "application/json" }),
|
||||
body: JSON.stringify({ url }),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// 媒体库:本人上传的全部图片
|
||||
export async function apiMyMedia(): Promise<{
|
||||
attachments?: MediaAttachment[];
|
||||
error?: string;
|
||||
}> {
|
||||
const res = await fetchWithRefresh("/api/my/media", {
|
||||
method: "GET",
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// 彻底删除本人附件
|
||||
export async function apiDeleteAttachment(
|
||||
id: number
|
||||
): Promise<{ ok?: boolean; error?: string }> {
|
||||
const res = await fetchWithRefresh(`/api/my/attachments/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function apiToggleLike(postId: string) {
|
||||
const res = await fetchWithRefresh(`/api/posts/${postId}/like`, {
|
||||
method: "POST",
|
||||
@@ -663,3 +738,23 @@ export async function apiAdminDeleteAnnouncement(id: number): Promise<void> {
|
||||
throw new Error(data.error || "删除公告失败");
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 站点外观设置 =====
|
||||
|
||||
export async function apiGetSettings(): Promise<PublicSettings> {
|
||||
const res = await fetchWithRefresh("/api/settings", { method: "GET" });
|
||||
if (!res.ok) throw new Error("获取站点设置失败");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// 更新站点主题色;accent 传空串恢复默认
|
||||
export async function apiUpdateSiteSettings(accent: string): Promise<PublicSettings> {
|
||||
const res = await fetchWithRefresh("/api/admin/settings", {
|
||||
method: "PUT",
|
||||
headers: clientHeaders({ "Content-Type": "application/json" }),
|
||||
body: JSON.stringify({ accent }),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "保存设置失败");
|
||||
return { accent: data.accent ?? "" };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user