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 => publicApi.post("/public/auth/register", data), login: (data: PublicLoginParams): Promise => publicApi.post("/public/auth/login", data), /** 手机号验证码登录 */ loginBySms: (data: PublicSmsLoginParams): Promise => publicApi.post("/public/auth/login/sms", data), /** 发送短信验证码 */ sendSmsCode: (phone: string): Promise => publicApi.post("/public/auth/sms/send", { phone }), }; // ==================== 个人信息 ==================== export const publicProfileApi = { getProfile: (): Promise => 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 => publicApi.get("/public/mine/children"), create: (data: CreateChildParams): Promise => publicApi.post("/public/mine/children", data), get: (id: number): Promise => publicApi.get(`/public/mine/children/${id}`), update: (id: number, data: Partial): Promise => 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 => publicApi.post("/public/children/create-account", data), // 获取子女账号列表 list: (): Promise => publicApi.get("/public/children/accounts"), // 家长切换到子女身份 switchToChild: (childUserId: number): Promise => 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 => 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 => 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 => 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 => publicApi.get(`/public/works/${id}`), // 更新作品 update: ( id: number, data: { title?: string; description?: string; coverUrl?: string; visibility?: string; tagIds?: number[]; }, ): Promise => 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 => 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 => 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 => publicApi.get("/public/tags"), hot: (): Promise => publicApi.get("/public/tags/hot"), }; // ==================== 作品广场 ==================== export const publicGalleryApi = { recommended: (): Promise => 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 => 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;