library-picturebook-activity/frontend/src/api/public.ts

646 lines
17 KiB
TypeScript
Raw Normal View History

import axios from "axios";
// 公众端专用 axios 实例
const publicApi = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || "/api",
timeout: 15000,
});
// 请求拦截器
publicApi.interceptors.request.use((config) => {
const token = localStorage.getItem("public_token");
if (token) {
// 检查 Token 是否过期
if (isTokenExpired(token)) {
localStorage.removeItem("public_token");
localStorage.removeItem("public_user");
// 如果在公众端页面,跳转到登录页
if (window.location.pathname.startsWith("/p/")) {
window.location.href = "/p/login";
}
return config;
}
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
/**
* JWT payload Token
*/
function isTokenExpired(token: string): boolean {
try {
const parts = token.split(".");
if (parts.length !== 3) return true;
const payload = JSON.parse(atob(parts[1]));
if (!payload.exp) return false;
// exp 是秒级时间戳,转换为毫秒比较
return Date.now() >= payload.exp * 1000;
} catch {
return true;
}
}
// 响应拦截器
publicApi.interceptors.response.use(
(response) => {
// 后端返回格式:{ code: 200, message: "success", data: xxx }
// 检查业务状态码,非 200 视为业务错误
const resData = response.data;
// 后端统一 Result 为 200乐读派 B2/B3 等原始体常用 0 表示成功(见 lesingle-aicreate-client
if (
resData &&
resData.code !== undefined &&
resData.code !== 200 &&
resData.code !== 0
) {
// 兼容后端 Result.message 和乐读派原始响应的 msg 字段
const error: any = new Error(
resData.message || resData.msg || "请求失败",
);
error.response = { data: resData };
return Promise.reject(error);
}
if (resData) {
return resData.data !== undefined ? resData.data : resData;
}
return resData;
},
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem("public_token");
localStorage.removeItem("public_user");
// 如果在公众端页面,跳转到公众端登录
if (window.location.pathname.startsWith("/p/")) {
window.location.href = "/p/login";
}
}
return Promise.reject(error);
},
);
// ==================== 认证 ====================
export interface PublicRegisterParams {
username: string;
password: string;
nickname: string;
phone?: string;
smsCode?: string;
city?: string;
}
export interface PublicLoginParams {
username: string;
password: string;
}
export interface PublicSmsLoginParams {
phone: string;
smsCode: string;
}
export interface PublicUser {
id: number;
username: string;
nickname: string;
phone: string | null;
city: string | null;
avatar: string | null;
tenantId: number;
tenantCode: string;
userSource: string;
userType: "adult" | "child";
parentUserId: number | null;
roles: string[];
permissions: string[];
children?: any[];
childrenCount?: number;
}
export interface LoginResponse {
token: string;
user: PublicUser;
}
export const publicAuthApi = {
register: (data: PublicRegisterParams): Promise<LoginResponse> =>
publicApi.post("/public/auth/register", data),
login: (data: PublicLoginParams): Promise<LoginResponse> =>
publicApi.post("/public/auth/login", data),
/** 手机号验证码登录 */
loginBySms: (data: PublicSmsLoginParams): Promise<LoginResponse> =>
publicApi.post("/public/auth/login/sms", data),
/** 发送短信验证码 */
sendSmsCode: (phone: string): Promise<void> =>
publicApi.post("/public/auth/sms/send", { phone }),
};
// ==================== 个人信息 ====================
export const publicProfileApi = {
getProfile: (): Promise<PublicUser> => publicApi.get("/public/mine/profile"),
updateProfile: (data: {
nickname?: string;
city?: string;
avatar?: string;
gender?: string;
}) => publicApi.put("/public/mine/profile", data),
};
// ==================== 子女管理 ====================
export interface Child {
id: number;
parentId: number;
name: string;
gender: string | null;
birthday: string | null;
grade: string | null;
city: string | null;
schoolName: string | null;
avatar: string | null;
}
export interface CreateChildParams {
name: string;
gender?: string;
birthday?: string;
grade?: string;
city?: string;
schoolName?: string;
}
export const publicChildrenApi = {
list: (): Promise<Child[]> => publicApi.get("/public/mine/children"),
create: (data: CreateChildParams): Promise<Child> =>
publicApi.post("/public/mine/children", data),
get: (id: number): Promise<Child> =>
publicApi.get(`/public/mine/children/${id}`),
update: (id: number, data: Partial<CreateChildParams>): Promise<Child> =>
publicApi.put(`/public/mine/children/${id}`, data),
delete: (id: number) => publicApi.delete(`/public/mine/children/${id}`),
};
// ==================== 子女独立账号管理 ====================
export interface CreateChildAccountParams {
username: string;
password: string;
nickname: string;
gender?: string;
birthday?: string;
city?: string;
avatar?: string;
relationship?: string;
}
export interface ChildAccount {
id: number;
username: string;
nickname: string;
avatar: string | null;
gender: string | null;
birthday: string | null;
city: string | null;
status: string;
userType: string;
createTime: string;
relationship: string | null;
controlMode: string;
}
export const publicChildAccountApi = {
// 家长为子女创建独立账号
create: (data: CreateChildAccountParams): Promise<any> =>
publicApi.post("/public/children/create-account", data),
// 获取子女账号列表
list: (): Promise<ChildAccount[]> =>
publicApi.get("/public/children/accounts"),
// 家长切换到子女身份
switchToChild: (childUserId: number): Promise<LoginResponse> =>
publicApi.post("/public/auth/switch-child", { childUserId }),
// 更新子女账号信息
update: (
id: number,
data: {
nickname?: string;
password?: string;
gender?: string;
birthday?: string;
city?: string;
avatar?: string;
controlMode?: string;
},
): Promise<any> => publicApi.put(`/public/children/accounts/${id}`, data),
// 子女查看家长信息
getParentInfo: (): Promise<{
parentId: number;
nickname: string;
avatar: string | null;
relationship: string | null;
} | null> => publicApi.get("/public/mine/parent-info"),
};
// ==================== 活动 ====================
export interface PublicActivity {
id: number;
contestName: string;
contestType: string;
contestState: string;
status: string;
startTime: string;
endTime: string;
coverUrl: string | null;
posterUrl: string | null;
registerStartTime: string;
registerEndTime: string;
submitStartTime: string;
submitEndTime: string;
submitRule: string;
reviewStartTime: string;
reviewEndTime: string;
organizers: any;
visibility: string;
resultState: string;
resultPublishTime: string | null;
content: string;
address: string | null;
contactName: string | null;
contactPhone: string | null;
contactQrcode: string | null;
coOrganizers: any;
sponsors: any;
registerState: string;
workType: string;
workRequirement: string;
}
/** 公众端活动详情(含公告、附件等扩展字段) */
export interface PublicActivityNotice {
id: number;
title: string;
content: string;
noticeType?: string;
publishTime?: string;
createTime?: string;
}
export interface PublicActivityAttachment {
id: number;
fileName: string;
fileUrl: string;
fileType?: string;
format?: string;
size?: string;
}
export interface PublicActivityDetail extends PublicActivity {
/** 兼容旧字段;详情正文以后端 content 为准 */
description?: string;
notices?: PublicActivityNotice[];
attachments?: PublicActivityAttachment[];
ageMin?: number;
ageMax?: number;
targetCities?: string[];
}
/** 公众端公示成果行(无报名账号等敏感字段) */
export interface PublicActivityResultItem {
id: number;
workNo: string | null;
title: string | null;
rank: number | null;
finalScore: number | string | null;
awardName: string | null;
participantName: string;
}
export const publicActivitiesApi = {
list: (params?: {
page?: number;
pageSize?: number;
keyword?: string;
contestType?: string;
}): Promise<{ list: PublicActivity[]; total: number }> =>
publicApi.get("/public/activities", { params }),
detail: (id: number): Promise<PublicActivityDetail> =>
publicApi.get(`/public/activities/${id}`),
register: (
id: number,
data: { participantType: "self" | "child"; childId?: number },
) => publicApi.post(`/public/activities/${id}/register`, data),
getMyRegistration: (id: number) =>
publicApi.get<{
id: number;
contestId: number;
userId: number;
registrationType: string;
registrationState: string;
registrationTime: string;
hasSubmittedWork: boolean;
workCount: number;
} | null>(`/public/activities/${id}/my-registration`),
submitWork: (
id: number,
data: {
registrationId: number;
userWorkId?: number;
title?: string;
description?: string;
files?: string[];
previewUrl?: string;
attachments?: {
fileName: string;
fileUrl: string;
fileType?: string;
size?: string;
}[];
},
) => publicApi.post(`/public/activities/${id}/submit-work`, data),
/** 公示成果分页(仅 resultState=published 的活动;无需登录) */
getPublishedResults: (
id: number,
params?: { page?: number; pageSize?: number },
): Promise<{
list: PublicActivityResultItem[];
total: number;
page: number;
pageSize: number;
}> => publicApi.get(`/public/activities/${id}/results`, { params }),
};
// ==================== 我的报名 ====================
export const publicMineApi = {
registrations: (params?: { page?: number; pageSize?: number }) =>
2026-04-03 18:47:42 +08:00
publicApi.get("/public/activities/mine/registrations", { params }),
};
// ==================== 点赞 & 收藏 ====================
export const publicInteractionApi = {
like: (workId: number) => publicApi.post(`/public/works/${workId}/like`),
favorite: (workId: number) =>
publicApi.post(`/public/works/${workId}/favorite`),
getInteraction: (workId: number) =>
publicApi.get(`/public/works/${workId}/interaction`),
batchStatus: (workIds: number[]) =>
publicApi.post("/public/works/batch-interaction", { workIds }),
myFavorites: (params?: { page?: number; pageSize?: number }) =>
publicApi.get("/public/mine/favorites", { params }),
};
// ==================== 用户作品库 ====================
/**
*
*
* 状态流转: draft unpublished pending_review published / rejected
* published unpublished
* rejected pending_review
*
* - draft 稿 / /
* - unpublished
* - pending_review
* - published
* - rejected
* - taken_down unpublished
*
* docs/design/public/ugc-work-status-redesign.md
*/
export type WorkStatus =
| "draft"
| "unpublished"
| "pending_review"
| "published"
| "rejected"
| "taken_down";
export interface UserWork {
id: number;
userId: number;
/** 乐读派 remote work id与创作路由参数一致 */
remoteWorkId?: string | null;
title: string;
coverUrl: string | null;
description: string | null;
visibility: string;
status: WorkStatus;
reviewNote: string | null;
originalImageUrl: string | null;
voiceInputUrl: string | null;
textInput: string | null;
aiMeta: any;
viewCount: number;
likeCount: number;
favoriteCount: number;
commentCount: number;
shareCount: number;
publishTime: string | null;
createTime: string;
modifyTime: string;
pages?: UserWorkPage[];
tags?: Array<{ tag: { id: number; name: string; category: string } }>;
creator?: {
id: number;
nickname: string;
avatar: string | null;
username: string;
};
_count?: {
pages: number;
likes: number;
favorites: number;
comments: number;
};
}
export interface UserWorkPage {
id: number;
workId: number;
pageNo: number;
imageUrl: string | null;
text: string | null;
audioUrl: string | null;
}
export const publicUserWorksApi = {
// 创建作品
create: (data: {
title: string;
coverUrl?: string;
description?: string;
visibility?: string;
originalImageUrl?: string;
voiceInputUrl?: string;
textInput?: string;
aiMeta?: any;
pages?: Array<{
pageNo: number;
imageUrl?: string;
text?: string;
audioUrl?: string;
}>;
tagIds?: number[];
}): Promise<UserWork> => publicApi.post("/public/works", data),
// 我的作品列表
list: (params?: {
page?: number;
pageSize?: number;
status?: string;
keyword?: string;
}): Promise<{ list: UserWork[]; total: number }> =>
publicApi.get("/public/works", { params }),
// 作品详情
detail: (id: number): Promise<UserWork> =>
publicApi.get(`/public/works/${id}`),
// 更新作品
update: (
id: number,
data: {
title?: string;
description?: string;
coverUrl?: string;
visibility?: string;
tagIds?: number[];
},
): Promise<UserWork> => publicApi.put(`/public/works/${id}`, data),
// 删除作品
delete: (id: number) => publicApi.delete(`/public/works/${id}`),
// 发布作品(进入审核)
publish: (id: number) => publicApi.post(`/public/works/${id}/publish`),
// 获取绘本分页
getPages: (id: number): Promise<UserWorkPage[]> =>
publicApi.get(`/public/works/${id}/pages`),
// 保存绘本分页
savePages: (
id: number,
pages: Array<{
pageNo: number;
imageUrl?: string;
text?: string;
audioUrl?: string;
}>,
) => publicApi.post(`/public/works/${id}/pages`, { pages }),
};
// ==================== AI 创作流程 ====================
export const publicCreationApi = {
// 提交创作请求(保留但降级为辅助接口)
submit: (data: {
originalImageUrl: string;
voiceInputUrl?: string;
textInput?: string;
}): Promise<{ id: number; status: string; message: string }> =>
publicApi.post("/public/creation/submit", data),
// 查询生成进度(返回 INT 类型 status + progress
getStatus: (
id: number,
): Promise<{
id: number;
status: number;
progress: number;
progressMessage: string | null;
remoteWorkId: string | null;
title: string;
coverUrl: string | null;
}> => publicApi.get(`/public/creation/${id}/status`),
// 获取生成结果(包含 pageList
getResult: (id: number): Promise<UserWork> =>
publicApi.get(`/public/creation/${id}/result`),
// 创作历史
history: (params?: {
page?: number;
pageSize?: number;
}): Promise<{ list: any[]; total: number }> =>
publicApi.get("/public/creation/history", { params }),
};
// ==================== 乐读派 AI 创作集成 ====================
export const leaiApi = {
// 获取乐读派创作 Tokeniframe 模式主入口)
getToken: (): Promise<{
token: string;
orgId: string;
}> => publicApi.get("/leai-auth/token"),
// 刷新 TokenTOKEN_EXPIRED 时调用)
refreshToken: (): Promise<{
token: string;
orgId: string;
}> => publicApi.get("/leai-auth/refresh-token"),
};
// ==================== 标签 ====================
export interface WorkTag {
id: number;
name: string;
category: string | null;
usageCount: number;
}
export const publicTagsApi = {
list: (): Promise<WorkTag[]> => publicApi.get("/public/tags"),
hot: (): Promise<WorkTag[]> => publicApi.get("/public/tags/hot"),
};
// ==================== 作品广场 ====================
export const publicGalleryApi = {
recommended: (): Promise<UserWork[]> =>
publicApi.get("/public/gallery/recommended"),
list: (params?: {
page?: number;
pageSize?: number;
tagId?: number;
category?: string;
sortBy?: string;
keyword?: string;
}): Promise<{ list: UserWork[]; total: number }> =>
publicApi.get("/public/gallery", { params }),
detail: (id: number): Promise<UserWork> =>
publicApi.get(`/public/gallery/${id}`),
userWorks: (
userId: number,
params?: { page?: number; pageSize?: number },
): Promise<{ list: UserWork[]; total: number }> =>
publicApi.get(`/public/users/${userId}/works`, { params }),
};
export default publicApi;