diff --git a/frontend/src/api/public.ts b/frontend/src/api/public.ts index f494033..5f179bb 100644 --- a/frontend/src/api/public.ts +++ b/frontend/src/api/public.ts @@ -1,43 +1,43 @@ -import axios from "axios" +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") + const token = localStorage.getItem("public_token"); if (token) { // 检查 Token 是否过期 if (isTokenExpired(token)) { - localStorage.removeItem("public_token") - localStorage.removeItem("public_user") + localStorage.removeItem("public_token"); + localStorage.removeItem("public_user"); // 如果在公众端页面,跳转到登录页 if (window.location.pathname.startsWith("/p/")) { - window.location.href = "/p/login" + window.location.href = "/p/login"; } - return config + return config; } - config.headers.Authorization = `Bearer ${token}` + config.headers.Authorization = `Bearer ${token}`; } - return config -}) + 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 + 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 + return Date.now() >= payload.exp * 1000; } catch { - return true + return true; } } @@ -46,73 +46,75 @@ publicApi.interceptors.response.use( (response) => { // 后端返回格式:{ code: 200, message: "success", data: xxx } // 检查业务状态码,非 200 视为业务错误 - const resData = response.data + const resData = response.data; if (resData && resData.code !== undefined && resData.code !== 200) { // 兼容后端 Result.message 和乐读派原始响应的 msg 字段 - const error: any = new Error(resData.message || resData.msg || "请求失败") - error.response = { data: resData } - return Promise.reject(error) + 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.data !== undefined ? resData.data : resData; } - return resData + return resData; }, (error) => { if (error.response?.status === 401) { - localStorage.removeItem("public_token") - localStorage.removeItem("public_user") + localStorage.removeItem("public_token"); + localStorage.removeItem("public_user"); // 如果在公众端页面,跳转到公众端登录 if (window.location.pathname.startsWith("/p/")) { - window.location.href = "/p/login" + window.location.href = "/p/login"; } } - return Promise.reject(error) + return Promise.reject(error); }, -) +); // ==================== 认证 ==================== export interface PublicRegisterParams { - username: string - password: string - nickname: string - phone?: string - smsCode?: string - city?: string + username: string; + password: string; + nickname: string; + phone?: string; + smsCode?: string; + city?: string; } export interface PublicLoginParams { - username: string - password: string + username: string; + password: string; } export interface PublicSmsLoginParams { - phone: string - smsCode: string + 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 + 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 + token: string; + user: PublicUser; } export const publicAuthApi = { @@ -129,7 +131,7 @@ export const publicAuthApi = { /** 发送短信验证码 */ sendSmsCode: (phone: string): Promise => publicApi.post("/public/auth/sms/send", { phone }), -} +}; // ==================== 个人信息 ==================== @@ -137,34 +139,34 @@ export const publicProfileApi = { getProfile: (): Promise => publicApi.get("/public/mine/profile"), updateProfile: (data: { - nickname?: string - city?: string - avatar?: string - gender?: string + 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 + 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 + name: string; + gender?: string; + birthday?: string; + grade?: string; + city?: string; + schoolName?: string; } export const publicChildrenApi = { @@ -180,34 +182,34 @@ export const publicChildrenApi = { 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 + 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 + 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 = { @@ -224,108 +226,109 @@ export const publicChildAccountApi = { 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), + 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"), -} + 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 + 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 + 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 + 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[] + 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 + 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 + page?: number; + pageSize?: number; + keyword?: string; + contestType?: string; }): Promise<{ list: PublicActivity[]; total: number }> => publicApi.get("/public/activities", { params }), @@ -339,26 +342,31 @@ export const publicActivitiesApi = { getMyRegistration: (id: number) => publicApi.get<{ - id: number - contestId: number - userId: number - registrationType: string - registrationState: string - registrationTime: string - hasSubmittedWork: boolean - workCount: number + 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 }[] + 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), @@ -367,25 +375,24 @@ export const publicActivitiesApi = { id: number, params?: { page?: number; pageSize?: number }, ): Promise<{ - list: PublicActivityResultItem[] - total: number - page: number - pageSize: number + 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`), + like: (workId: number) => publicApi.post(`/public/works/${workId}/like`), favorite: (workId: number) => publicApi.post(`/public/works/${workId}/favorite`), getInteraction: (workId: number) => @@ -394,7 +401,7 @@ export const publicInteractionApi = { publicApi.post("/public/works/batch-interaction", { workIds }), myFavorites: (params?: { page?: number; pageSize?: number }) => publicApi.get("/public/mine/favorites", { params }), -} +}; // ==================== 用户作品库 ==================== @@ -415,70 +422,85 @@ export const publicInteractionApi = { * 详见 docs/design/public/ugc-work-status-redesign.md */ export type WorkStatus = - | 'draft' - | 'unpublished' - | 'pending_review' - | 'published' - | 'rejected' - | 'taken_down' + | "draft" + | "unpublished" + | "pending_review" + | "published" + | "rejected" + | "taken_down"; export interface UserWork { - id: number - userId: number - 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 } + id: number; + userId: number; + 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 + 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[] + 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 + page?: number; + pageSize?: number; + status?: string; + keyword?: string; }): Promise<{ list: UserWork[]; total: number }> => publicApi.get("/public/works", { params }), @@ -487,13 +509,16 @@ export const publicUserWorksApi = { 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), + 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}`), @@ -506,30 +531,39 @@ export const publicUserWorksApi = { 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 }), -} + 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 + 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 + 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) @@ -537,39 +571,42 @@ export const publicCreationApi = { publicApi.get(`/public/creation/${id}/result`), // 创作历史 - history: (params?: { page?: number; pageSize?: number }): Promise<{ list: any[]; total: number }> => + 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 + token: string; + orgId: string; }> => publicApi.get("/leai-auth/token"), // 刷新 Token(TOKEN_EXPIRED 时调用) refreshToken: (): Promise<{ - token: string - orgId: string + token: string; + orgId: string; }> => publicApi.get("/leai-auth/refresh-token"), -} +}; // ==================== 标签 ==================== export interface WorkTag { - id: number - name: string - category: string | null - usageCount: number + 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"), -} +}; // ==================== 作品广场 ==================== @@ -578,20 +615,23 @@ export const publicGalleryApi = { publicApi.get("/public/gallery/recommended"), list: (params?: { - page?: number - pageSize?: number - tagId?: number - category?: string - sortBy?: string - keyword?: string + 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 }> => + userWorks: ( + userId: number, + params?: { page?: number; pageSize?: number }, + ): Promise<{ list: UserWork[]; total: number }> => publicApi.get(`/public/users/${userId}/works`, { params }), -} +}; -export default publicApi +export default publicApi; diff --git a/frontend/src/stores/aicreate.ts b/frontend/src/stores/aicreate.ts index 2925589..dfc3203 100644 --- a/frontend/src/stores/aicreate.ts +++ b/frontend/src/stores/aicreate.ts @@ -6,6 +6,7 @@ */ import { defineStore } from 'pinia' import { ref } from 'vue' +import { clearExtractDraft } from '@/utils/aicreate/extractDraft' export const useAicreateStore = defineStore('aicreate', () => { // ─── 认证信息(不再存储敏感信息到 localStorage) ─── @@ -20,6 +21,8 @@ export const useAicreateStore = defineStore('aicreate', () => { const selectedStyle = ref('') const storyData = ref(null) const workId = ref('') + /** extract 接口可能返回的 workId,供下游使用 */ + const originalWorkId = ref('') const workDetail = ref(null) // ─── Tab 切换状态保存 ─── @@ -56,12 +59,14 @@ export const useAicreateStore = defineStore('aicreate', () => { selectedStyle.value = '' storyData.value = null workId.value = '' + originalWorkId.value = '' workDetail.value = null lastCreateRoute.value = '' // 只清除创作流程数据,保留认证信息 localStorage.removeItem('le_workId') // 清除 sessionStorage 中的恢复数据 sessionStorage.removeItem('le_recovery') + clearExtractDraft() } function saveRecoveryState() { @@ -186,7 +191,7 @@ export const useAicreateStore = defineStore('aicreate', () => { setSession, clearSession, // 创作流程 imageUrl, extractId, characters, selectedCharacter, - selectedStyle, storyData, workId, workDetail, + selectedStyle, storyData, workId, originalWorkId, workDetail, reset, saveRecoveryState, restoreRecoveryState, // 开发模式 fillMockData, diff --git a/frontend/src/utils/aicreate/extractDraft.ts b/frontend/src/utils/aicreate/extractDraft.ts new file mode 100644 index 0000000..9cd0764 --- /dev/null +++ b/frontend/src/utils/aicreate/extractDraft.ts @@ -0,0 +1,60 @@ +/** + * 角色提取(extract)结果本地草稿:用于断线后继续创作,10 天过期 + */ +const STORAGE_KEY = 'le_extract_draft' +const TEN_DAYS_MS = 10 * 24 * 60 * 60 * 1000 + +export interface ExtractDraftPayload { + savedAt: number + imageUrl: string + extractId: string + characters: any[] + /** 接口原始响应,便于扩展 */ + raw?: unknown +} + +export function saveExtractDraft( + payload: Omit & { savedAt?: number } +): void { + const data: ExtractDraftPayload = { + savedAt: payload.savedAt ?? Date.now(), + imageUrl: payload.imageUrl, + extractId: payload.extractId ?? '', + characters: payload.characters ?? [], + ...(payload.raw !== undefined ? { raw: payload.raw } : {}), + } + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(data)) + } catch { + // quota / 隐私模式等忽略 + } +} + +/** 未过期返回草稿并校验字段;过期或损坏则删除并返回 null */ +export function loadExtractDraft(): ExtractDraftPayload | null { + const raw = localStorage.getItem(STORAGE_KEY) + if (!raw) return null + try { + const data = JSON.parse(raw) as ExtractDraftPayload + if (!data.savedAt || Date.now() - data.savedAt > TEN_DAYS_MS) { + localStorage.removeItem(STORAGE_KEY) + return null + } + if (!data.imageUrl || !Array.isArray(data.characters)) { + localStorage.removeItem(STORAGE_KEY) + return null + } + return data + } catch { + localStorage.removeItem(STORAGE_KEY) + return null + } +} + +export function clearExtractDraft(): void { + try { + localStorage.removeItem(STORAGE_KEY) + } catch { + /* ignore */ + } +} diff --git a/frontend/src/views/public/create/views/CharactersView.vue b/frontend/src/views/public/create/views/CharactersView.vue index 07abc49..72758d9 100644 --- a/frontend/src/views/public/create/views/CharactersView.vue +++ b/frontend/src/views/public/create/views/CharactersView.vue @@ -6,14 +6,6 @@ export default { name: 'CharactersView' }
- -
- - Mock 角色数 - - -
-
@@ -34,10 +26,15 @@ export default { name: 'CharactersView' }