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

646 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 }) =>
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;