646 lines
17 KiB
TypeScript
646 lines
17 KiB
TypeScript
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 = {
|
||
// 获取乐读派创作 Token(iframe 模式主入口)
|
||
getToken: (): Promise<{
|
||
token: string;
|
||
orgId: string;
|
||
}> => publicApi.get("/leai-auth/token"),
|
||
|
||
// 刷新 Token(TOKEN_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;
|