library-picturebook-activity/frontend/src/api/public.ts
aid cd8de97f79 feat: 引入未发布作品状态与状态化操作面板(前端 UI 第一阶段)
- 新增 docs/design/public/ugc-work-status-redesign.md 完整设计方案与状态流转图
- UserWork.status 类型化为 WorkStatus 联合类型,加入 unpublished 中间状态
- 作品库 Index.vue 加「未发布」tab + 紫色标签样式 + emptyDescription + dev mock 兜底
- Detail.vue 完整重写:清 emoji + 紫粉化 + 根据 status 切换 5 套操作按钮
  · draft → 继续创作
  · unpublished → 公开发布 / 编辑信息
  · pending_review → 撤回审核
  · published → 下架
  · rejected → 修改后重交(含拒绝原因卡片)
- EditInfoView 三按钮语义调整:「保存」→ unpublished、「直接发布」→ pending_review
- 删除独立 Publish.vue 与对应路由(发布功能并入 Detail.vue 公开发布按钮)
- 新建 _dev-mock.ts dev 模式数据共享文件,5 条覆盖全状态的 mock 作品 + 13 页详情
- 撤回 / 下架等接口与 leai workId 映射留 TODO,待后端第二阶段联调

详见 docs/design/public/ugc-work-status-redesign.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 18:48:14 +08:00

580 lines
15 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 }
// 当 data 为 null 时,直接返回 null
if (response.data) {
return response.data.data !== undefined ? response.data.data : response.data
}
return response.data
},
(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
city?: string
}
export interface PublicLoginParams {
username: string
password: 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),
}
// ==================== 个人信息 ====================
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
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
h5Url: string
phone: string
}> => publicApi.get("/leai-auth/token"),
// 刷新 TokenTOKEN_EXPIRED 时调用)
refreshToken: (): Promise<{
token: string
orgId: string
phone: 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