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