feat: 选角仅放大镜预览、extract草稿与作品提交审核
Made-with: Cursor
This commit is contained in:
parent
df1817fe23
commit
7ad98e92ea
@ -1,43 +1,43 @@
|
|||||||
import axios from "axios"
|
import axios from "axios";
|
||||||
|
|
||||||
// 公众端专用 axios 实例
|
// 公众端专用 axios 实例
|
||||||
const publicApi = axios.create({
|
const publicApi = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_BASE_URL || "/api",
|
baseURL: import.meta.env.VITE_API_BASE_URL || "/api",
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
})
|
});
|
||||||
|
|
||||||
// 请求拦截器
|
// 请求拦截器
|
||||||
publicApi.interceptors.request.use((config) => {
|
publicApi.interceptors.request.use((config) => {
|
||||||
const token = localStorage.getItem("public_token")
|
const token = localStorage.getItem("public_token");
|
||||||
if (token) {
|
if (token) {
|
||||||
// 检查 Token 是否过期
|
// 检查 Token 是否过期
|
||||||
if (isTokenExpired(token)) {
|
if (isTokenExpired(token)) {
|
||||||
localStorage.removeItem("public_token")
|
localStorage.removeItem("public_token");
|
||||||
localStorage.removeItem("public_user")
|
localStorage.removeItem("public_user");
|
||||||
// 如果在公众端页面,跳转到登录页
|
// 如果在公众端页面,跳转到登录页
|
||||||
if (window.location.pathname.startsWith("/p/")) {
|
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 是否过期
|
* 解析 JWT payload 检查 Token 是否过期
|
||||||
*/
|
*/
|
||||||
function isTokenExpired(token: string): boolean {
|
function isTokenExpired(token: string): boolean {
|
||||||
try {
|
try {
|
||||||
const parts = token.split(".")
|
const parts = token.split(".");
|
||||||
if (parts.length !== 3) return true
|
if (parts.length !== 3) return true;
|
||||||
const payload = JSON.parse(atob(parts[1]))
|
const payload = JSON.parse(atob(parts[1]));
|
||||||
if (!payload.exp) return false
|
if (!payload.exp) return false;
|
||||||
// exp 是秒级时间戳,转换为毫秒比较
|
// exp 是秒级时间戳,转换为毫秒比较
|
||||||
return Date.now() >= payload.exp * 1000
|
return Date.now() >= payload.exp * 1000;
|
||||||
} catch {
|
} catch {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,73 +46,75 @@ publicApi.interceptors.response.use(
|
|||||||
(response) => {
|
(response) => {
|
||||||
// 后端返回格式:{ code: 200, message: "success", data: xxx }
|
// 后端返回格式:{ code: 200, message: "success", data: xxx }
|
||||||
// 检查业务状态码,非 200 视为业务错误
|
// 检查业务状态码,非 200 视为业务错误
|
||||||
const resData = response.data
|
const resData = response.data;
|
||||||
if (resData && resData.code !== undefined && resData.code !== 200) {
|
if (resData && resData.code !== undefined && resData.code !== 200) {
|
||||||
// 兼容后端 Result.message 和乐读派原始响应的 msg 字段
|
// 兼容后端 Result.message 和乐读派原始响应的 msg 字段
|
||||||
const error: any = new Error(resData.message || resData.msg || "请求失败")
|
const error: any = new Error(
|
||||||
error.response = { data: resData }
|
resData.message || resData.msg || "请求失败",
|
||||||
return Promise.reject(error)
|
);
|
||||||
|
error.response = { data: resData };
|
||||||
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
if (resData) {
|
if (resData) {
|
||||||
return resData.data !== undefined ? resData.data : resData
|
return resData.data !== undefined ? resData.data : resData;
|
||||||
}
|
}
|
||||||
return resData
|
return resData;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
if (error.response?.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
localStorage.removeItem("public_token")
|
localStorage.removeItem("public_token");
|
||||||
localStorage.removeItem("public_user")
|
localStorage.removeItem("public_user");
|
||||||
// 如果在公众端页面,跳转到公众端登录
|
// 如果在公众端页面,跳转到公众端登录
|
||||||
if (window.location.pathname.startsWith("/p/")) {
|
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 {
|
export interface PublicRegisterParams {
|
||||||
username: string
|
username: string;
|
||||||
password: string
|
password: string;
|
||||||
nickname: string
|
nickname: string;
|
||||||
phone?: string
|
phone?: string;
|
||||||
smsCode?: string
|
smsCode?: string;
|
||||||
city?: string
|
city?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PublicLoginParams {
|
export interface PublicLoginParams {
|
||||||
username: string
|
username: string;
|
||||||
password: string
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PublicSmsLoginParams {
|
export interface PublicSmsLoginParams {
|
||||||
phone: string
|
phone: string;
|
||||||
smsCode: string
|
smsCode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PublicUser {
|
export interface PublicUser {
|
||||||
id: number
|
id: number;
|
||||||
username: string
|
username: string;
|
||||||
nickname: string
|
nickname: string;
|
||||||
phone: string | null
|
phone: string | null;
|
||||||
city: string | null
|
city: string | null;
|
||||||
avatar: string | null
|
avatar: string | null;
|
||||||
tenantId: number
|
tenantId: number;
|
||||||
tenantCode: string
|
tenantCode: string;
|
||||||
userSource: string
|
userSource: string;
|
||||||
userType: "adult" | "child"
|
userType: "adult" | "child";
|
||||||
parentUserId: number | null
|
parentUserId: number | null;
|
||||||
roles: string[]
|
roles: string[];
|
||||||
permissions: string[]
|
permissions: string[];
|
||||||
children?: any[]
|
children?: any[];
|
||||||
childrenCount?: number
|
childrenCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
token: string
|
token: string;
|
||||||
user: PublicUser
|
user: PublicUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const publicAuthApi = {
|
export const publicAuthApi = {
|
||||||
@ -129,7 +131,7 @@ export const publicAuthApi = {
|
|||||||
/** 发送短信验证码 */
|
/** 发送短信验证码 */
|
||||||
sendSmsCode: (phone: string): Promise<void> =>
|
sendSmsCode: (phone: string): Promise<void> =>
|
||||||
publicApi.post("/public/auth/sms/send", { phone }),
|
publicApi.post("/public/auth/sms/send", { phone }),
|
||||||
}
|
};
|
||||||
|
|
||||||
// ==================== 个人信息 ====================
|
// ==================== 个人信息 ====================
|
||||||
|
|
||||||
@ -137,34 +139,34 @@ export const publicProfileApi = {
|
|||||||
getProfile: (): Promise<PublicUser> => publicApi.get("/public/mine/profile"),
|
getProfile: (): Promise<PublicUser> => publicApi.get("/public/mine/profile"),
|
||||||
|
|
||||||
updateProfile: (data: {
|
updateProfile: (data: {
|
||||||
nickname?: string
|
nickname?: string;
|
||||||
city?: string
|
city?: string;
|
||||||
avatar?: string
|
avatar?: string;
|
||||||
gender?: string
|
gender?: string;
|
||||||
}) => publicApi.put("/public/mine/profile", data),
|
}) => publicApi.put("/public/mine/profile", data),
|
||||||
}
|
};
|
||||||
|
|
||||||
// ==================== 子女管理 ====================
|
// ==================== 子女管理 ====================
|
||||||
|
|
||||||
export interface Child {
|
export interface Child {
|
||||||
id: number
|
id: number;
|
||||||
parentId: number
|
parentId: number;
|
||||||
name: string
|
name: string;
|
||||||
gender: string | null
|
gender: string | null;
|
||||||
birthday: string | null
|
birthday: string | null;
|
||||||
grade: string | null
|
grade: string | null;
|
||||||
city: string | null
|
city: string | null;
|
||||||
schoolName: string | null
|
schoolName: string | null;
|
||||||
avatar: string | null
|
avatar: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateChildParams {
|
export interface CreateChildParams {
|
||||||
name: string
|
name: string;
|
||||||
gender?: string
|
gender?: string;
|
||||||
birthday?: string
|
birthday?: string;
|
||||||
grade?: string
|
grade?: string;
|
||||||
city?: string
|
city?: string;
|
||||||
schoolName?: string
|
schoolName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const publicChildrenApi = {
|
export const publicChildrenApi = {
|
||||||
@ -180,34 +182,34 @@ export const publicChildrenApi = {
|
|||||||
publicApi.put(`/public/mine/children/${id}`, data),
|
publicApi.put(`/public/mine/children/${id}`, data),
|
||||||
|
|
||||||
delete: (id: number) => publicApi.delete(`/public/mine/children/${id}`),
|
delete: (id: number) => publicApi.delete(`/public/mine/children/${id}`),
|
||||||
}
|
};
|
||||||
|
|
||||||
// ==================== 子女独立账号管理 ====================
|
// ==================== 子女独立账号管理 ====================
|
||||||
|
|
||||||
export interface CreateChildAccountParams {
|
export interface CreateChildAccountParams {
|
||||||
username: string
|
username: string;
|
||||||
password: string
|
password: string;
|
||||||
nickname: string
|
nickname: string;
|
||||||
gender?: string
|
gender?: string;
|
||||||
birthday?: string
|
birthday?: string;
|
||||||
city?: string
|
city?: string;
|
||||||
avatar?: string
|
avatar?: string;
|
||||||
relationship?: string
|
relationship?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChildAccount {
|
export interface ChildAccount {
|
||||||
id: number
|
id: number;
|
||||||
username: string
|
username: string;
|
||||||
nickname: string
|
nickname: string;
|
||||||
avatar: string | null
|
avatar: string | null;
|
||||||
gender: string | null
|
gender: string | null;
|
||||||
birthday: string | null
|
birthday: string | null;
|
||||||
city: string | null
|
city: string | null;
|
||||||
status: string
|
status: string;
|
||||||
userType: string
|
userType: string;
|
||||||
createTime: string
|
createTime: string;
|
||||||
relationship: string | null
|
relationship: string | null;
|
||||||
controlMode: string
|
controlMode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const publicChildAccountApi = {
|
export const publicChildAccountApi = {
|
||||||
@ -224,108 +226,109 @@ export const publicChildAccountApi = {
|
|||||||
publicApi.post("/public/auth/switch-child", { childUserId }),
|
publicApi.post("/public/auth/switch-child", { childUserId }),
|
||||||
|
|
||||||
// 更新子女账号信息
|
// 更新子女账号信息
|
||||||
update: (id: number, data: {
|
update: (
|
||||||
nickname?: string
|
id: number,
|
||||||
password?: string
|
data: {
|
||||||
gender?: string
|
nickname?: string;
|
||||||
birthday?: string
|
password?: string;
|
||||||
city?: string
|
gender?: string;
|
||||||
avatar?: string
|
birthday?: string;
|
||||||
controlMode?: string
|
city?: string;
|
||||||
}): Promise<any> =>
|
avatar?: string;
|
||||||
publicApi.put(`/public/children/accounts/${id}`, data),
|
controlMode?: string;
|
||||||
|
},
|
||||||
|
): Promise<any> => publicApi.put(`/public/children/accounts/${id}`, data),
|
||||||
|
|
||||||
// 子女查看家长信息
|
// 子女查看家长信息
|
||||||
getParentInfo: (): Promise<{
|
getParentInfo: (): Promise<{
|
||||||
parentId: number
|
parentId: number;
|
||||||
nickname: string
|
nickname: string;
|
||||||
avatar: string | null
|
avatar: string | null;
|
||||||
relationship: string | null
|
relationship: string | null;
|
||||||
} | null> =>
|
} | null> => publicApi.get("/public/mine/parent-info"),
|
||||||
publicApi.get("/public/mine/parent-info"),
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 活动 ====================
|
// ==================== 活动 ====================
|
||||||
|
|
||||||
export interface PublicActivity {
|
export interface PublicActivity {
|
||||||
id: number
|
id: number;
|
||||||
contestName: string
|
contestName: string;
|
||||||
contestType: string
|
contestType: string;
|
||||||
contestState: string
|
contestState: string;
|
||||||
status: string
|
status: string;
|
||||||
startTime: string
|
startTime: string;
|
||||||
endTime: string
|
endTime: string;
|
||||||
coverUrl: string | null
|
coverUrl: string | null;
|
||||||
posterUrl: string | null
|
posterUrl: string | null;
|
||||||
registerStartTime: string
|
registerStartTime: string;
|
||||||
registerEndTime: string
|
registerEndTime: string;
|
||||||
submitStartTime: string
|
submitStartTime: string;
|
||||||
submitEndTime: string
|
submitEndTime: string;
|
||||||
submitRule: string
|
submitRule: string;
|
||||||
reviewStartTime: string
|
reviewStartTime: string;
|
||||||
reviewEndTime: string
|
reviewEndTime: string;
|
||||||
organizers: any
|
organizers: any;
|
||||||
visibility: string
|
visibility: string;
|
||||||
resultState: string
|
resultState: string;
|
||||||
resultPublishTime: string | null
|
resultPublishTime: string | null;
|
||||||
content: string
|
content: string;
|
||||||
address: string | null
|
address: string | null;
|
||||||
contactName: string | null
|
contactName: string | null;
|
||||||
contactPhone: string | null
|
contactPhone: string | null;
|
||||||
contactQrcode: string | null
|
contactQrcode: string | null;
|
||||||
coOrganizers: any
|
coOrganizers: any;
|
||||||
sponsors: any
|
sponsors: any;
|
||||||
registerState: string
|
registerState: string;
|
||||||
workType: string
|
workType: string;
|
||||||
workRequirement: string
|
workRequirement: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 公众端活动详情(含公告、附件等扩展字段) */
|
/** 公众端活动详情(含公告、附件等扩展字段) */
|
||||||
export interface PublicActivityNotice {
|
export interface PublicActivityNotice {
|
||||||
id: number
|
id: number;
|
||||||
title: string
|
title: string;
|
||||||
content: string
|
content: string;
|
||||||
noticeType?: string
|
noticeType?: string;
|
||||||
publishTime?: string
|
publishTime?: string;
|
||||||
createTime?: string
|
createTime?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PublicActivityAttachment {
|
export interface PublicActivityAttachment {
|
||||||
id: number
|
id: number;
|
||||||
fileName: string
|
fileName: string;
|
||||||
fileUrl: string
|
fileUrl: string;
|
||||||
fileType?: string
|
fileType?: string;
|
||||||
format?: string
|
format?: string;
|
||||||
size?: string
|
size?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PublicActivityDetail extends PublicActivity {
|
export interface PublicActivityDetail extends PublicActivity {
|
||||||
/** 兼容旧字段;详情正文以后端 content 为准 */
|
/** 兼容旧字段;详情正文以后端 content 为准 */
|
||||||
description?: string
|
description?: string;
|
||||||
notices?: PublicActivityNotice[]
|
notices?: PublicActivityNotice[];
|
||||||
attachments?: PublicActivityAttachment[]
|
attachments?: PublicActivityAttachment[];
|
||||||
ageMin?: number
|
ageMin?: number;
|
||||||
ageMax?: number
|
ageMax?: number;
|
||||||
targetCities?: string[]
|
targetCities?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 公众端公示成果行(无报名账号等敏感字段) */
|
/** 公众端公示成果行(无报名账号等敏感字段) */
|
||||||
export interface PublicActivityResultItem {
|
export interface PublicActivityResultItem {
|
||||||
id: number
|
id: number;
|
||||||
workNo: string | null
|
workNo: string | null;
|
||||||
title: string | null
|
title: string | null;
|
||||||
rank: number | null
|
rank: number | null;
|
||||||
finalScore: number | string | null
|
finalScore: number | string | null;
|
||||||
awardName: string | null
|
awardName: string | null;
|
||||||
participantName: string
|
participantName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const publicActivitiesApi = {
|
export const publicActivitiesApi = {
|
||||||
list: (params?: {
|
list: (params?: {
|
||||||
page?: number
|
page?: number;
|
||||||
pageSize?: number
|
pageSize?: number;
|
||||||
keyword?: string
|
keyword?: string;
|
||||||
contestType?: string
|
contestType?: string;
|
||||||
}): Promise<{ list: PublicActivity[]; total: number }> =>
|
}): Promise<{ list: PublicActivity[]; total: number }> =>
|
||||||
publicApi.get("/public/activities", { params }),
|
publicApi.get("/public/activities", { params }),
|
||||||
|
|
||||||
@ -339,26 +342,31 @@ export const publicActivitiesApi = {
|
|||||||
|
|
||||||
getMyRegistration: (id: number) =>
|
getMyRegistration: (id: number) =>
|
||||||
publicApi.get<{
|
publicApi.get<{
|
||||||
id: number
|
id: number;
|
||||||
contestId: number
|
contestId: number;
|
||||||
userId: number
|
userId: number;
|
||||||
registrationType: string
|
registrationType: string;
|
||||||
registrationState: string
|
registrationState: string;
|
||||||
registrationTime: string
|
registrationTime: string;
|
||||||
hasSubmittedWork: boolean
|
hasSubmittedWork: boolean;
|
||||||
workCount: number
|
workCount: number;
|
||||||
} | null>(`/public/activities/${id}/my-registration`),
|
} | null>(`/public/activities/${id}/my-registration`),
|
||||||
|
|
||||||
submitWork: (
|
submitWork: (
|
||||||
id: number,
|
id: number,
|
||||||
data: {
|
data: {
|
||||||
registrationId: number
|
registrationId: number;
|
||||||
userWorkId?: number
|
userWorkId?: number;
|
||||||
title?: string
|
title?: string;
|
||||||
description?: string
|
description?: string;
|
||||||
files?: string[]
|
files?: string[];
|
||||||
previewUrl?: string
|
previewUrl?: string;
|
||||||
attachments?: { fileName: string; fileUrl: string; fileType?: string; size?: string }[]
|
attachments?: {
|
||||||
|
fileName: string;
|
||||||
|
fileUrl: string;
|
||||||
|
fileType?: string;
|
||||||
|
size?: string;
|
||||||
|
}[];
|
||||||
},
|
},
|
||||||
) => publicApi.post(`/public/activities/${id}/submit-work`, data),
|
) => publicApi.post(`/public/activities/${id}/submit-work`, data),
|
||||||
|
|
||||||
@ -367,25 +375,24 @@ export const publicActivitiesApi = {
|
|||||||
id: number,
|
id: number,
|
||||||
params?: { page?: number; pageSize?: number },
|
params?: { page?: number; pageSize?: number },
|
||||||
): Promise<{
|
): Promise<{
|
||||||
list: PublicActivityResultItem[]
|
list: PublicActivityResultItem[];
|
||||||
total: number
|
total: number;
|
||||||
page: number
|
page: number;
|
||||||
pageSize: number
|
pageSize: number;
|
||||||
}> => publicApi.get(`/public/activities/${id}/results`, { params }),
|
}> => publicApi.get(`/public/activities/${id}/results`, { params }),
|
||||||
}
|
};
|
||||||
|
|
||||||
// ==================== 我的报名 ====================
|
// ==================== 我的报名 ====================
|
||||||
|
|
||||||
export const publicMineApi = {
|
export const publicMineApi = {
|
||||||
registrations: (params?: { page?: number; pageSize?: number }) =>
|
registrations: (params?: { page?: number; pageSize?: number }) =>
|
||||||
publicApi.get("/public/activities/mine/registrations", { params }),
|
publicApi.get("/public/activities/mine/registrations", { params }),
|
||||||
}
|
};
|
||||||
|
|
||||||
// ==================== 点赞 & 收藏 ====================
|
// ==================== 点赞 & 收藏 ====================
|
||||||
|
|
||||||
export const publicInteractionApi = {
|
export const publicInteractionApi = {
|
||||||
like: (workId: number) =>
|
like: (workId: number) => publicApi.post(`/public/works/${workId}/like`),
|
||||||
publicApi.post(`/public/works/${workId}/like`),
|
|
||||||
favorite: (workId: number) =>
|
favorite: (workId: number) =>
|
||||||
publicApi.post(`/public/works/${workId}/favorite`),
|
publicApi.post(`/public/works/${workId}/favorite`),
|
||||||
getInteraction: (workId: number) =>
|
getInteraction: (workId: number) =>
|
||||||
@ -394,7 +401,7 @@ export const publicInteractionApi = {
|
|||||||
publicApi.post("/public/works/batch-interaction", { workIds }),
|
publicApi.post("/public/works/batch-interaction", { workIds }),
|
||||||
myFavorites: (params?: { page?: number; pageSize?: number }) =>
|
myFavorites: (params?: { page?: number; pageSize?: number }) =>
|
||||||
publicApi.get("/public/mine/favorites", { params }),
|
publicApi.get("/public/mine/favorites", { params }),
|
||||||
}
|
};
|
||||||
|
|
||||||
// ==================== 用户作品库 ====================
|
// ==================== 用户作品库 ====================
|
||||||
|
|
||||||
@ -415,70 +422,85 @@ export const publicInteractionApi = {
|
|||||||
* 详见 docs/design/public/ugc-work-status-redesign.md
|
* 详见 docs/design/public/ugc-work-status-redesign.md
|
||||||
*/
|
*/
|
||||||
export type WorkStatus =
|
export type WorkStatus =
|
||||||
| 'draft'
|
| "draft"
|
||||||
| 'unpublished'
|
| "unpublished"
|
||||||
| 'pending_review'
|
| "pending_review"
|
||||||
| 'published'
|
| "published"
|
||||||
| 'rejected'
|
| "rejected"
|
||||||
| 'taken_down'
|
| "taken_down";
|
||||||
|
|
||||||
export interface UserWork {
|
export interface UserWork {
|
||||||
id: number
|
id: number;
|
||||||
userId: number
|
userId: number;
|
||||||
title: string
|
title: string;
|
||||||
coverUrl: string | null
|
coverUrl: string | null;
|
||||||
description: string | null
|
description: string | null;
|
||||||
visibility: string
|
visibility: string;
|
||||||
status: WorkStatus
|
status: WorkStatus;
|
||||||
reviewNote: string | null
|
reviewNote: string | null;
|
||||||
originalImageUrl: string | null
|
originalImageUrl: string | null;
|
||||||
voiceInputUrl: string | null
|
voiceInputUrl: string | null;
|
||||||
textInput: string | null
|
textInput: string | null;
|
||||||
aiMeta: any
|
aiMeta: any;
|
||||||
viewCount: number
|
viewCount: number;
|
||||||
likeCount: number
|
likeCount: number;
|
||||||
favoriteCount: number
|
favoriteCount: number;
|
||||||
commentCount: number
|
commentCount: number;
|
||||||
shareCount: number
|
shareCount: number;
|
||||||
publishTime: string | null
|
publishTime: string | null;
|
||||||
createTime: string
|
createTime: string;
|
||||||
modifyTime: string
|
modifyTime: string;
|
||||||
pages?: UserWorkPage[]
|
pages?: UserWorkPage[];
|
||||||
tags?: Array<{ tag: { id: number; name: string; category: string } }>
|
tags?: Array<{ tag: { id: number; name: string; category: string } }>;
|
||||||
creator?: { id: number; nickname: string; avatar: string | null; username: string }
|
creator?: {
|
||||||
_count?: { pages: number; likes: number; favorites: number; comments: number }
|
id: number;
|
||||||
|
nickname: string;
|
||||||
|
avatar: string | null;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
_count?: {
|
||||||
|
pages: number;
|
||||||
|
likes: number;
|
||||||
|
favorites: number;
|
||||||
|
comments: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserWorkPage {
|
export interface UserWorkPage {
|
||||||
id: number
|
id: number;
|
||||||
workId: number
|
workId: number;
|
||||||
pageNo: number
|
pageNo: number;
|
||||||
imageUrl: string | null
|
imageUrl: string | null;
|
||||||
text: string | null
|
text: string | null;
|
||||||
audioUrl: string | null
|
audioUrl: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const publicUserWorksApi = {
|
export const publicUserWorksApi = {
|
||||||
// 创建作品
|
// 创建作品
|
||||||
create: (data: {
|
create: (data: {
|
||||||
title: string
|
title: string;
|
||||||
coverUrl?: string
|
coverUrl?: string;
|
||||||
description?: string
|
description?: string;
|
||||||
visibility?: string
|
visibility?: string;
|
||||||
originalImageUrl?: string
|
originalImageUrl?: string;
|
||||||
voiceInputUrl?: string
|
voiceInputUrl?: string;
|
||||||
textInput?: string
|
textInput?: string;
|
||||||
aiMeta?: any
|
aiMeta?: any;
|
||||||
pages?: Array<{ pageNo: number; imageUrl?: string; text?: string; audioUrl?: string }>
|
pages?: Array<{
|
||||||
tagIds?: number[]
|
pageNo: number;
|
||||||
|
imageUrl?: string;
|
||||||
|
text?: string;
|
||||||
|
audioUrl?: string;
|
||||||
|
}>;
|
||||||
|
tagIds?: number[];
|
||||||
}): Promise<UserWork> => publicApi.post("/public/works", data),
|
}): Promise<UserWork> => publicApi.post("/public/works", data),
|
||||||
|
|
||||||
// 我的作品列表
|
// 我的作品列表
|
||||||
list: (params?: {
|
list: (params?: {
|
||||||
page?: number
|
page?: number;
|
||||||
pageSize?: number
|
pageSize?: number;
|
||||||
status?: string
|
status?: string;
|
||||||
keyword?: string
|
keyword?: string;
|
||||||
}): Promise<{ list: UserWork[]; total: number }> =>
|
}): Promise<{ list: UserWork[]; total: number }> =>
|
||||||
publicApi.get("/public/works", { params }),
|
publicApi.get("/public/works", { params }),
|
||||||
|
|
||||||
@ -487,13 +509,16 @@ export const publicUserWorksApi = {
|
|||||||
publicApi.get(`/public/works/${id}`),
|
publicApi.get(`/public/works/${id}`),
|
||||||
|
|
||||||
// 更新作品
|
// 更新作品
|
||||||
update: (id: number, data: {
|
update: (
|
||||||
title?: string
|
id: number,
|
||||||
description?: string
|
data: {
|
||||||
coverUrl?: string
|
title?: string;
|
||||||
visibility?: string
|
description?: string;
|
||||||
tagIds?: number[]
|
coverUrl?: string;
|
||||||
}): Promise<UserWork> => publicApi.put(`/public/works/${id}`, data),
|
visibility?: string;
|
||||||
|
tagIds?: number[];
|
||||||
|
},
|
||||||
|
): Promise<UserWork> => publicApi.put(`/public/works/${id}`, data),
|
||||||
|
|
||||||
// 删除作品
|
// 删除作品
|
||||||
delete: (id: number) => publicApi.delete(`/public/works/${id}`),
|
delete: (id: number) => publicApi.delete(`/public/works/${id}`),
|
||||||
@ -506,30 +531,39 @@ export const publicUserWorksApi = {
|
|||||||
publicApi.get(`/public/works/${id}/pages`),
|
publicApi.get(`/public/works/${id}/pages`),
|
||||||
|
|
||||||
// 保存绘本分页
|
// 保存绘本分页
|
||||||
savePages: (id: number, pages: Array<{ pageNo: number; imageUrl?: string; text?: string; audioUrl?: string }>) =>
|
savePages: (
|
||||||
publicApi.post(`/public/works/${id}/pages`, { pages }),
|
id: number,
|
||||||
}
|
pages: Array<{
|
||||||
|
pageNo: number;
|
||||||
|
imageUrl?: string;
|
||||||
|
text?: string;
|
||||||
|
audioUrl?: string;
|
||||||
|
}>,
|
||||||
|
) => publicApi.post(`/public/works/${id}/pages`, { pages }),
|
||||||
|
};
|
||||||
|
|
||||||
// ==================== AI 创作流程 ====================
|
// ==================== AI 创作流程 ====================
|
||||||
|
|
||||||
export const publicCreationApi = {
|
export const publicCreationApi = {
|
||||||
// 提交创作请求(保留但降级为辅助接口)
|
// 提交创作请求(保留但降级为辅助接口)
|
||||||
submit: (data: {
|
submit: (data: {
|
||||||
originalImageUrl: string
|
originalImageUrl: string;
|
||||||
voiceInputUrl?: string
|
voiceInputUrl?: string;
|
||||||
textInput?: string
|
textInput?: string;
|
||||||
}): Promise<{ id: number; status: string; message: string }> =>
|
}): Promise<{ id: number; status: string; message: string }> =>
|
||||||
publicApi.post("/public/creation/submit", data),
|
publicApi.post("/public/creation/submit", data),
|
||||||
|
|
||||||
// 查询生成进度(返回 INT 类型 status + progress)
|
// 查询生成进度(返回 INT 类型 status + progress)
|
||||||
getStatus: (id: number): Promise<{
|
getStatus: (
|
||||||
id: number
|
id: number,
|
||||||
status: number
|
): Promise<{
|
||||||
progress: number
|
id: number;
|
||||||
progressMessage: string | null
|
status: number;
|
||||||
remoteWorkId: string | null
|
progress: number;
|
||||||
title: string
|
progressMessage: string | null;
|
||||||
coverUrl: string | null
|
remoteWorkId: string | null;
|
||||||
|
title: string;
|
||||||
|
coverUrl: string | null;
|
||||||
}> => publicApi.get(`/public/creation/${id}/status`),
|
}> => publicApi.get(`/public/creation/${id}/status`),
|
||||||
|
|
||||||
// 获取生成结果(包含 pageList)
|
// 获取生成结果(包含 pageList)
|
||||||
@ -537,39 +571,42 @@ export const publicCreationApi = {
|
|||||||
publicApi.get(`/public/creation/${id}/result`),
|
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 }),
|
publicApi.get("/public/creation/history", { params }),
|
||||||
}
|
};
|
||||||
|
|
||||||
// ==================== 乐读派 AI 创作集成 ====================
|
// ==================== 乐读派 AI 创作集成 ====================
|
||||||
|
|
||||||
export const leaiApi = {
|
export const leaiApi = {
|
||||||
// 获取乐读派创作 Token(iframe 模式主入口)
|
// 获取乐读派创作 Token(iframe 模式主入口)
|
||||||
getToken: (): Promise<{
|
getToken: (): Promise<{
|
||||||
token: string
|
token: string;
|
||||||
orgId: string
|
orgId: string;
|
||||||
}> => publicApi.get("/leai-auth/token"),
|
}> => publicApi.get("/leai-auth/token"),
|
||||||
|
|
||||||
// 刷新 Token(TOKEN_EXPIRED 时调用)
|
// 刷新 Token(TOKEN_EXPIRED 时调用)
|
||||||
refreshToken: (): Promise<{
|
refreshToken: (): Promise<{
|
||||||
token: string
|
token: string;
|
||||||
orgId: string
|
orgId: string;
|
||||||
}> => publicApi.get("/leai-auth/refresh-token"),
|
}> => publicApi.get("/leai-auth/refresh-token"),
|
||||||
}
|
};
|
||||||
|
|
||||||
// ==================== 标签 ====================
|
// ==================== 标签 ====================
|
||||||
|
|
||||||
export interface WorkTag {
|
export interface WorkTag {
|
||||||
id: number
|
id: number;
|
||||||
name: string
|
name: string;
|
||||||
category: string | null
|
category: string | null;
|
||||||
usageCount: number
|
usageCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const publicTagsApi = {
|
export const publicTagsApi = {
|
||||||
list: (): Promise<WorkTag[]> => publicApi.get("/public/tags"),
|
list: (): Promise<WorkTag[]> => publicApi.get("/public/tags"),
|
||||||
hot: (): Promise<WorkTag[]> => publicApi.get("/public/tags/hot"),
|
hot: (): Promise<WorkTag[]> => publicApi.get("/public/tags/hot"),
|
||||||
}
|
};
|
||||||
|
|
||||||
// ==================== 作品广场 ====================
|
// ==================== 作品广场 ====================
|
||||||
|
|
||||||
@ -578,20 +615,23 @@ export const publicGalleryApi = {
|
|||||||
publicApi.get("/public/gallery/recommended"),
|
publicApi.get("/public/gallery/recommended"),
|
||||||
|
|
||||||
list: (params?: {
|
list: (params?: {
|
||||||
page?: number
|
page?: number;
|
||||||
pageSize?: number
|
pageSize?: number;
|
||||||
tagId?: number
|
tagId?: number;
|
||||||
category?: string
|
category?: string;
|
||||||
sortBy?: string
|
sortBy?: string;
|
||||||
keyword?: string
|
keyword?: string;
|
||||||
}): Promise<{ list: UserWork[]; total: number }> =>
|
}): Promise<{ list: UserWork[]; total: number }> =>
|
||||||
publicApi.get("/public/gallery", { params }),
|
publicApi.get("/public/gallery", { params }),
|
||||||
|
|
||||||
detail: (id: number): Promise<UserWork> =>
|
detail: (id: number): Promise<UserWork> =>
|
||||||
publicApi.get(`/public/gallery/${id}`),
|
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 }),
|
publicApi.get(`/public/users/${userId}/works`, { params }),
|
||||||
}
|
};
|
||||||
|
|
||||||
export default publicApi
|
export default publicApi;
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { clearExtractDraft } from '@/utils/aicreate/extractDraft'
|
||||||
|
|
||||||
export const useAicreateStore = defineStore('aicreate', () => {
|
export const useAicreateStore = defineStore('aicreate', () => {
|
||||||
// ─── 认证信息(不再存储敏感信息到 localStorage) ───
|
// ─── 认证信息(不再存储敏感信息到 localStorage) ───
|
||||||
@ -20,6 +21,8 @@ export const useAicreateStore = defineStore('aicreate', () => {
|
|||||||
const selectedStyle = ref('')
|
const selectedStyle = ref('')
|
||||||
const storyData = ref<any>(null)
|
const storyData = ref<any>(null)
|
||||||
const workId = ref('')
|
const workId = ref('')
|
||||||
|
/** extract 接口可能返回的 workId,供下游使用 */
|
||||||
|
const originalWorkId = ref('')
|
||||||
const workDetail = ref<any>(null)
|
const workDetail = ref<any>(null)
|
||||||
|
|
||||||
// ─── Tab 切换状态保存 ───
|
// ─── Tab 切换状态保存 ───
|
||||||
@ -56,12 +59,14 @@ export const useAicreateStore = defineStore('aicreate', () => {
|
|||||||
selectedStyle.value = ''
|
selectedStyle.value = ''
|
||||||
storyData.value = null
|
storyData.value = null
|
||||||
workId.value = ''
|
workId.value = ''
|
||||||
|
originalWorkId.value = ''
|
||||||
workDetail.value = null
|
workDetail.value = null
|
||||||
lastCreateRoute.value = ''
|
lastCreateRoute.value = ''
|
||||||
// 只清除创作流程数据,保留认证信息
|
// 只清除创作流程数据,保留认证信息
|
||||||
localStorage.removeItem('le_workId')
|
localStorage.removeItem('le_workId')
|
||||||
// 清除 sessionStorage 中的恢复数据
|
// 清除 sessionStorage 中的恢复数据
|
||||||
sessionStorage.removeItem('le_recovery')
|
sessionStorage.removeItem('le_recovery')
|
||||||
|
clearExtractDraft()
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveRecoveryState() {
|
function saveRecoveryState() {
|
||||||
@ -186,7 +191,7 @@ export const useAicreateStore = defineStore('aicreate', () => {
|
|||||||
setSession, clearSession,
|
setSession, clearSession,
|
||||||
// 创作流程
|
// 创作流程
|
||||||
imageUrl, extractId, characters, selectedCharacter,
|
imageUrl, extractId, characters, selectedCharacter,
|
||||||
selectedStyle, storyData, workId, workDetail,
|
selectedStyle, storyData, workId, originalWorkId, workDetail,
|
||||||
reset, saveRecoveryState, restoreRecoveryState,
|
reset, saveRecoveryState, restoreRecoveryState,
|
||||||
// 开发模式
|
// 开发模式
|
||||||
fillMockData,
|
fillMockData,
|
||||||
|
|||||||
60
frontend/src/utils/aicreate/extractDraft.ts
Normal file
60
frontend/src/utils/aicreate/extractDraft.ts
Normal file
@ -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<ExtractDraftPayload, 'savedAt'> & { 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 */
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,14 +6,6 @@ export default { name: 'CharactersView' }
|
|||||||
<PageHeader title="画作角色" :subtitle="subtitle" :step="1" />
|
<PageHeader title="画作角色" :subtitle="subtitle" :step="1" />
|
||||||
|
|
||||||
<div class="content page-content">
|
<div class="content page-content">
|
||||||
<!-- 开发模式:mock 数据切换 -->
|
|
||||||
<div v-if="isDev" class="dev-bar">
|
|
||||||
<experiment-outlined />
|
|
||||||
<span class="dev-label">Mock 角色数</span>
|
|
||||||
<button class="dev-btn" :class="{ active: characters.length === 1 }" @click="regenMock(1)">1 个</button>
|
|
||||||
<button class="dev-btn" :class="{ active: characters.length === 3 }" @click="regenMock(3)">3 个</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 加载中 -->
|
<!-- 加载中 -->
|
||||||
<div v-if="loading" class="loading-state">
|
<div v-if="loading" class="loading-state">
|
||||||
<loading-outlined class="loading-spinner" spin />
|
<loading-outlined class="loading-spinner" spin />
|
||||||
@ -34,10 +26,15 @@ export default { name: 'CharactersView' }
|
|||||||
<template v-else-if="characters.length === 1">
|
<template v-else-if="characters.length === 1">
|
||||||
<div class="single-wrap">
|
<div class="single-wrap">
|
||||||
<div class="single-card">
|
<div class="single-card">
|
||||||
<div class="single-img-wrap" @click="characters[0].originalCropUrl && (previewImg = characters[0].originalCropUrl)">
|
<div class="single-img-wrap">
|
||||||
<img v-if="characters[0].originalCropUrl" :src="characters[0].originalCropUrl" class="single-img" />
|
<img v-if="characters[0].originalCropUrl" :src="characters[0].originalCropUrl" class="single-img" />
|
||||||
<user-outlined v-else class="single-placeholder" />
|
<user-outlined v-else class="single-placeholder" />
|
||||||
<div class="zoom-hint"><zoom-in-outlined /></div>
|
<div
|
||||||
|
class="zoom-hint"
|
||||||
|
@click.stop="characters[0].originalCropUrl && (previewImg = characters[0].originalCropUrl)"
|
||||||
|
>
|
||||||
|
<zoom-in-outlined />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="single-tip">
|
<div class="single-tip">
|
||||||
@ -72,10 +69,15 @@ export default { name: 'CharactersView' }
|
|||||||
<check-outlined />
|
<check-outlined />
|
||||||
</div>
|
</div>
|
||||||
<!-- 头像 -->
|
<!-- 头像 -->
|
||||||
<div class="char-img-wrap" @click.stop="c.originalCropUrl && (previewImg = c.originalCropUrl)">
|
<div class="char-img-wrap">
|
||||||
<img v-if="c.originalCropUrl" :src="c.originalCropUrl" class="char-img" />
|
<img v-if="c.originalCropUrl" :src="c.originalCropUrl" class="char-img" />
|
||||||
<user-outlined v-else class="char-placeholder" />
|
<user-outlined v-else class="char-placeholder" />
|
||||||
<div class="zoom-hint"><zoom-in-outlined /></div>
|
<div
|
||||||
|
class="zoom-hint"
|
||||||
|
@click.stop="c.originalCropUrl && (previewImg = c.originalCropUrl)"
|
||||||
|
>
|
||||||
|
<zoom-in-outlined />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -110,13 +112,12 @@ import {
|
|||||||
CheckOutlined,
|
CheckOutlined,
|
||||||
ArrowRightOutlined,
|
ArrowRightOutlined,
|
||||||
ZoomInOutlined,
|
ZoomInOutlined,
|
||||||
ExperimentOutlined,
|
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
|
|
||||||
const isDev = import.meta.env.DEV
|
|
||||||
import PageHeader from '@/components/aicreate/PageHeader.vue'
|
import PageHeader from '@/components/aicreate/PageHeader.vue'
|
||||||
import { useAicreateStore } from '@/stores/aicreate'
|
import { useAicreateStore } from '@/stores/aicreate'
|
||||||
import { extractCharacters } from '@/api/aicreate'
|
import { extractCharacters } from '@/api/aicreate'
|
||||||
|
import { saveExtractDraft } from '@/utils/aicreate/extractDraft'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const store = useAicreateStore()
|
const store = useAicreateStore()
|
||||||
@ -170,6 +171,14 @@ onMounted(async () => {
|
|||||||
store.extractId = data.extractId || ''
|
store.extractId = data.extractId || ''
|
||||||
store.characters = characters.value
|
store.characters = characters.value
|
||||||
autoSelect()
|
autoSelect()
|
||||||
|
if (characters.value.length > 0) {
|
||||||
|
saveExtractDraft({
|
||||||
|
imageUrl: store.imageUrl,
|
||||||
|
extractId: store.extractId,
|
||||||
|
characters: characters.value,
|
||||||
|
raw: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error.value = '角色识别失败:' + (e.message || '请检查网络')
|
error.value = '角色识别失败:' + (e.message || '请检查网络')
|
||||||
} finally {
|
} finally {
|
||||||
@ -186,15 +195,6 @@ function autoSelect() {
|
|||||||
if (hero) selected.value = hero.charId
|
if (hero) selected.value = hero.charId
|
||||||
}
|
}
|
||||||
|
|
||||||
const regenMock = (count: number) => {
|
|
||||||
store.fillMockData(count)
|
|
||||||
characters.value = store.characters
|
|
||||||
selected.value = null
|
|
||||||
error.value = ''
|
|
||||||
loading.value = false
|
|
||||||
autoSelect()
|
|
||||||
}
|
|
||||||
|
|
||||||
const goNext = () => {
|
const goNext = () => {
|
||||||
// 单角色直接用第一个,多角色用 selected
|
// 单角色直接用第一个,多角色用 selected
|
||||||
const target = characters.value.length === 1
|
const target = characters.value.length === 1
|
||||||
@ -219,51 +219,6 @@ const goNext = () => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 开发模式切换器 ---------- */
|
|
||||||
.dev-bar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
background: rgba(99, 102, 241, 0.04);
|
|
||||||
border: 1px dashed rgba(99, 102, 241, 0.3);
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--ai-text-sub);
|
|
||||||
|
|
||||||
:deep(.anticon) {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--ai-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.dev-label {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.dev-btn {
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: #fff;
|
|
||||||
color: var(--ai-text-sub);
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: var(--ai-primary);
|
|
||||||
color: var(--ai-primary);
|
|
||||||
}
|
|
||||||
&.active {
|
|
||||||
background: var(--ai-gradient);
|
|
||||||
border-color: transparent;
|
|
||||||
color: #fff;
|
|
||||||
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 加载状态 ---------- */
|
/* ---------- 加载状态 ---------- */
|
||||||
.loading-state {
|
.loading-state {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -343,7 +298,6 @@ const goNext = () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: zoom-in;
|
|
||||||
|
|
||||||
&:hover .zoom-hint { opacity: 1; }
|
&:hover .zoom-hint { opacity: 1; }
|
||||||
&:active { transform: scale(0.98); }
|
&:active { transform: scale(0.98); }
|
||||||
@ -440,7 +394,6 @@ const goNext = () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: zoom-in;
|
|
||||||
|
|
||||||
&:hover .zoom-hint { opacity: 1; }
|
&:hover .zoom-hint { opacity: 1; }
|
||||||
}
|
}
|
||||||
@ -494,8 +447,9 @@ const goNext = () => {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
right: 6px;
|
right: 6px;
|
||||||
bottom: 6px;
|
bottom: 6px;
|
||||||
width: 22px;
|
z-index: 3;
|
||||||
height: 22px;
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: rgba(15, 12, 41, 0.55);
|
background: rgba(15, 12, 41, 0.55);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@ -504,6 +458,7 @@ const goNext = () => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
|
cursor: pointer;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,15 +3,6 @@ export default { name: 'CreatingView' }
|
|||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="creating-page">
|
<div class="creating-page">
|
||||||
<!-- 开发模式:状态切换 -->
|
|
||||||
<div v-if="isDev" class="dev-bar">
|
|
||||||
<experiment-outlined />
|
|
||||||
<span class="dev-label">Mock</span>
|
|
||||||
<button class="dev-btn" @click="enterMockProgress">进度</button>
|
|
||||||
<button class="dev-btn" @click="enterMockError">错误</button>
|
|
||||||
<button class="dev-btn" @click="goMockPreview">跳到预览</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 进度环 -->
|
<!-- 进度环 -->
|
||||||
<div class="ring-wrap">
|
<div class="ring-wrap">
|
||||||
<svg width="180" height="180" class="ring-svg">
|
<svg width="180" height="180" class="ring-svg">
|
||||||
@ -98,7 +89,6 @@ import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { Client } from '@stomp/stompjs'
|
import { Client } from '@stomp/stompjs'
|
||||||
import {
|
import {
|
||||||
ExperimentOutlined,
|
|
||||||
FrownOutlined,
|
FrownOutlined,
|
||||||
WifiOutlined,
|
WifiOutlined,
|
||||||
CloudServerOutlined,
|
CloudServerOutlined,
|
||||||
@ -107,6 +97,7 @@ import {
|
|||||||
import { useAicreateStore } from '@/stores/aicreate'
|
import { useAicreateStore } from '@/stores/aicreate'
|
||||||
import { createStory, getWorkDetail } from '@/api/aicreate'
|
import { createStory, getWorkDetail } from '@/api/aicreate'
|
||||||
import { STATUS, getRouteByStatus } from '@/utils/aicreate/status'
|
import { STATUS, getRouteByStatus } from '@/utils/aicreate/status'
|
||||||
|
import { clearExtractDraft } from '@/utils/aicreate/extractDraft'
|
||||||
import config from '@/utils/aicreate/config'
|
import config from '@/utils/aicreate/config'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -130,8 +121,6 @@ const creatingTips = [
|
|||||||
'色彩正在调和',
|
'色彩正在调和',
|
||||||
]
|
]
|
||||||
|
|
||||||
const isDev = import.meta.env.DEV
|
|
||||||
|
|
||||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||||
let dotTimer: ReturnType<typeof setInterval> | null = null
|
let dotTimer: ReturnType<typeof setInterval> | null = null
|
||||||
let tipTimer: ReturnType<typeof setInterval> | null = null
|
let tipTimer: ReturnType<typeof setInterval> | null = null
|
||||||
@ -194,6 +183,15 @@ function restoreWorkId() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 创作已推进到预览/配音等后续步骤时清除 extract 本地草稿 */
|
||||||
|
function replaceWhenCreationAdvances(route: ReturnType<typeof getRouteByStatus>) {
|
||||||
|
if (!route) return
|
||||||
|
if (route.name !== 'PublicCreateCreating') {
|
||||||
|
clearExtractDraft()
|
||||||
|
}
|
||||||
|
setTimeout(() => router.replace(route), 800)
|
||||||
|
}
|
||||||
|
|
||||||
// ─── WebSocket 实时推送 (首次进入使用) ───
|
// ─── WebSocket 实时推送 (首次进入使用) ───
|
||||||
const startWebSocket = (workId: string) => {
|
const startWebSocket = (workId: string) => {
|
||||||
wsDegraded = false
|
wsDegraded = false
|
||||||
@ -285,7 +283,7 @@ const startPolling = (workId: string) => {
|
|||||||
pollTimer = null
|
pollTimer = null
|
||||||
saveWorkId('')
|
saveWorkId('')
|
||||||
const route = getRouteByStatus(work.status, workId)
|
const route = getRouteByStatus(work.status, workId)
|
||||||
if (route) setTimeout(() => router.replace(route), 800)
|
if (route) replaceWhenCreationAdvances(route)
|
||||||
} else if (work.status === STATUS.FAILED) {
|
} else if (work.status === STATUS.FAILED) {
|
||||||
clearInterval(pollTimer!)
|
clearInterval(pollTimer!)
|
||||||
pollTimer = null
|
pollTimer = null
|
||||||
@ -358,10 +356,6 @@ const resumePolling = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const retry = () => {
|
const retry = () => {
|
||||||
if (isDev && !store.imageUrl) {
|
|
||||||
enterMockProgress()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
saveWorkId('')
|
saveWorkId('')
|
||||||
submitted = false
|
submitted = false
|
||||||
startCreation()
|
startCreation()
|
||||||
@ -374,31 +368,6 @@ const leaveToWorks = () => {
|
|||||||
router.push('/p/works?tab=draft')
|
router.push('/p/works?tab=draft')
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 开发模式:模拟状态 ───
|
|
||||||
const enterMockProgress = () => {
|
|
||||||
closeWebSocket()
|
|
||||||
if (pollTimer) { clearInterval(pollTimer); pollTimer = null }
|
|
||||||
submitted = true
|
|
||||||
error.value = ''
|
|
||||||
networkWarn.value = false
|
|
||||||
progress.value = 35
|
|
||||||
stage.value = '正在编写故事…'
|
|
||||||
}
|
|
||||||
|
|
||||||
const enterMockError = () => {
|
|
||||||
closeWebSocket()
|
|
||||||
if (pollTimer) { clearInterval(pollTimer); pollTimer = null }
|
|
||||||
submitted = false
|
|
||||||
error.value = '创作请求异常,请返回重新操作'
|
|
||||||
}
|
|
||||||
|
|
||||||
const goMockPreview = () => {
|
|
||||||
closeWebSocket()
|
|
||||||
if (pollTimer) { clearInterval(pollTimer); pollTimer = null }
|
|
||||||
store.fillMockWorkDetail()
|
|
||||||
router.push(`/p/create/preview/${store.workId}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
dotTimer = setInterval(() => {
|
dotTimer = setInterval(() => {
|
||||||
dots.value = dots.value.length >= 3 ? '' : dots.value + '.'
|
dots.value = dots.value.length >= 3 ? '' : dots.value + '.'
|
||||||
@ -416,12 +385,6 @@ onMounted(() => {
|
|||||||
restoreWorkId()
|
restoreWorkId()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开发模式兜底:缺关键数据时直接进入模拟态,避免真实接口失败
|
|
||||||
if (isDev && !store.workId && (!store.imageUrl || !store.storyData)) {
|
|
||||||
enterMockProgress()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (store.workId) {
|
if (store.workId) {
|
||||||
submitted = true
|
submitted = true
|
||||||
progress.value = 10
|
progress.value = 10
|
||||||
@ -452,46 +415,6 @@ onUnmounted(() => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 开发模式切换器 ---------- */
|
|
||||||
.dev-bar {
|
|
||||||
position: absolute;
|
|
||||||
top: 16px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 6px 12px;
|
|
||||||
background: rgba(99, 102, 241, 0.04);
|
|
||||||
border: 1px dashed rgba(99, 102, 241, 0.3);
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--ai-text-sub);
|
|
||||||
z-index: 5;
|
|
||||||
|
|
||||||
:deep(.anticon) {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--ai-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.dev-label { font-weight: 600; }
|
|
||||||
.dev-btn {
|
|
||||||
padding: 4px 10px;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: #fff;
|
|
||||||
color: var(--ai-text-sub);
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: var(--ai-primary);
|
|
||||||
color: var(--ai-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 进度环 ---------- */
|
/* ---------- 进度环 ---------- */
|
||||||
.ring-wrap {
|
.ring-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@ -291,7 +291,7 @@ function togglePlay() {
|
|||||||
|
|
||||||
// dev mock 兜底:mock 音频直接 toast 不真实播放
|
// dev mock 兜底:mock 音频直接 toast 不真实播放
|
||||||
if (typeof src === 'string' && src.startsWith('mock-audio-')) {
|
if (typeof src === 'string' && src.startsWith('mock-audio-')) {
|
||||||
showToast('开发模式:模拟音频暂不支持播放')
|
showToast('模拟音频暂不支持播放')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -161,6 +161,7 @@ import {
|
|||||||
import PageHeader from '@/components/aicreate/PageHeader.vue'
|
import PageHeader from '@/components/aicreate/PageHeader.vue'
|
||||||
import { getWorkDetail, updateWork } from '@/api/aicreate'
|
import { getWorkDetail, updateWork } from '@/api/aicreate'
|
||||||
import { useAicreateStore } from '@/stores/aicreate'
|
import { useAicreateStore } from '@/stores/aicreate'
|
||||||
|
import { clearExtractDraft } from '@/utils/aicreate/extractDraft'
|
||||||
import { STATUS, getRouteByStatus } from '@/utils/aicreate/status'
|
import { STATUS, getRouteByStatus } from '@/utils/aicreate/status'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -363,6 +364,7 @@ async function handlePublish() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
clearExtractDraft()
|
||||||
loadWork()
|
loadWork()
|
||||||
nextTick(() => { if (tagInput.value) tagInput.value.focus() })
|
nextTick(() => { if (tagInput.value) tagInput.value.focus() })
|
||||||
})
|
})
|
||||||
|
|||||||
@ -6,16 +6,6 @@ export default { name: 'UploadView' }
|
|||||||
<PageHeader title="上传作品" subtitle="上传你的画作,AI 自动识别角色" :step="0" />
|
<PageHeader title="上传作品" subtitle="上传你的画作,AI 自动识别角色" :step="0" />
|
||||||
|
|
||||||
<div class="content page-content">
|
<div class="content page-content">
|
||||||
<!-- 开发模式:跳过真实后端调用 -->
|
|
||||||
<div v-if="isDev" class="dev-skip">
|
|
||||||
<span class="dev-skip-label">
|
|
||||||
<experiment-outlined />
|
|
||||||
开发模式
|
|
||||||
</span>
|
|
||||||
<button class="dev-skip-btn" @click="handleSkipUpload(3)">跳过 · 3 个角色</button>
|
|
||||||
<button class="dev-skip-btn" @click="handleSkipUpload(1)">跳过 · 1 个角色</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-if="!preview">
|
<template v-if="!preview">
|
||||||
<!-- 上传区域 -->
|
<!-- 上传区域 -->
|
||||||
<div class="upload-area card">
|
<div class="upload-area card">
|
||||||
@ -118,6 +108,7 @@ import { useRouter } from 'vue-router'
|
|||||||
import PageHeader from '@/components/aicreate/PageHeader.vue'
|
import PageHeader from '@/components/aicreate/PageHeader.vue'
|
||||||
import { useAicreateStore } from '@/stores/aicreate'
|
import { useAicreateStore } from '@/stores/aicreate'
|
||||||
import { extractCharacters, ossUpload, checkQuota } from '@/api/aicreate'
|
import { extractCharacters, ossUpload, checkQuota } from '@/api/aicreate'
|
||||||
|
import { saveExtractDraft } from '@/utils/aicreate/extractDraft'
|
||||||
import {
|
import {
|
||||||
PictureOutlined,
|
PictureOutlined,
|
||||||
CameraOutlined,
|
CameraOutlined,
|
||||||
@ -130,16 +121,8 @@ import {
|
|||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
BulbOutlined,
|
BulbOutlined,
|
||||||
ArrowRightOutlined,
|
ArrowRightOutlined,
|
||||||
ExperimentOutlined,
|
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
|
|
||||||
const isDev = import.meta.env.DEV
|
|
||||||
|
|
||||||
const handleSkipUpload = (count: number) => {
|
|
||||||
store.fillMockData(count)
|
|
||||||
router.push('/p/create/characters')
|
|
||||||
}
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const store = useAicreateStore()
|
const store = useAicreateStore()
|
||||||
const preview = ref<string | null>(null)
|
const preview = ref<string | null>(null)
|
||||||
@ -325,6 +308,12 @@ const goNext = async () => {
|
|||||||
store.characters = chars
|
store.characters = chars
|
||||||
store.imageUrl = ossUrl
|
store.imageUrl = ossUrl
|
||||||
if (data.workId) store.originalWorkId = data.workId
|
if (data.workId) store.originalWorkId = data.workId
|
||||||
|
saveExtractDraft({
|
||||||
|
imageUrl: ossUrl,
|
||||||
|
extractId: data.extractId || '',
|
||||||
|
characters: chars,
|
||||||
|
raw: data,
|
||||||
|
})
|
||||||
router.push('/p/create/characters')
|
router.push('/p/create/characters')
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
uploadError.value = '识别失败:' + sanitizeError(e.message)
|
uploadError.value = '识别失败:' + sanitizeError(e.message)
|
||||||
@ -545,37 +534,4 @@ const goNext = async () => {
|
|||||||
:deep(.anticon) { font-size: 16px; }
|
:deep(.anticon) { font-size: 16px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 开发模式跳过按钮 */
|
|
||||||
.dev-skip {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.dev-skip-label {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--ai-text-sub);
|
|
||||||
:deep(.anticon) { font-size: 12px; }
|
|
||||||
}
|
|
||||||
.dev-skip-btn {
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 14px;
|
|
||||||
background: rgba(99, 102, 241, 0.06);
|
|
||||||
color: var(--ai-primary);
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
border: 1px dashed rgba(99, 102, 241, 0.32);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
&:hover {
|
|
||||||
background: rgba(99, 102, 241, 0.1);
|
|
||||||
border-color: var(--ai-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -142,16 +142,32 @@ import {
|
|||||||
StarOutlined,
|
StarOutlined,
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import { useAicreateStore } from '@/stores/aicreate'
|
import { useAicreateStore } from '@/stores/aicreate'
|
||||||
|
import { loadExtractDraft } from '@/utils/aicreate/extractDraft'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const store = useAicreateStore()
|
const store = useAicreateStore()
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 检查恢复状态
|
// 检查恢复状态(短会话内 Tab 切换等)
|
||||||
const recovery = store.restoreRecoveryState()
|
const recovery = store.restoreRecoveryState()
|
||||||
if (recovery && recovery.path && recovery.path !== '/') {
|
if (recovery && recovery.path && recovery.path !== '/') {
|
||||||
const newPath = '/p/create' + recovery.path
|
const newPath = '/p/create' + recovery.path
|
||||||
router.push(newPath)
|
router.push(newPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 角色提取草稿(10 天内):继续到选角页
|
||||||
|
const draft = loadExtractDraft()
|
||||||
|
if (draft && store.sessionToken) {
|
||||||
|
store.imageUrl = draft.imageUrl
|
||||||
|
store.extractId = draft.extractId
|
||||||
|
store.characters = draft.characters
|
||||||
|
store.selectedCharacter = null
|
||||||
|
store.storyData = null
|
||||||
|
store.selectedStyle = ''
|
||||||
|
store.workId = ''
|
||||||
|
store.workDetail = null
|
||||||
|
localStorage.removeItem('le_workId')
|
||||||
|
router.replace('/p/create/characters')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -41,7 +41,8 @@
|
|||||||
<!-- 绘本阅读器 -->
|
<!-- 绘本阅读器 -->
|
||||||
<div class="book-reader" v-if="work.pages && work.pages.length > 0">
|
<div class="book-reader" v-if="work.pages && work.pages.length > 0">
|
||||||
<div class="page-display">
|
<div class="page-display">
|
||||||
<img v-if="currentPageData?.imageUrl" :src="currentPageData.imageUrl" :alt="`第${currentPageIndex + 1}页`" class="page-image" />
|
<img v-if="currentPageData?.imageUrl" :src="currentPageData.imageUrl" :alt="`第${currentPageIndex + 1}页`"
|
||||||
|
class="page-image" />
|
||||||
<div v-else class="page-placeholder">
|
<div v-else class="page-placeholder">
|
||||||
<picture-outlined />
|
<picture-outlined />
|
||||||
</div>
|
</div>
|
||||||
@ -113,71 +114,43 @@
|
|||||||
<!-- 作者私有操作 -->
|
<!-- 作者私有操作 -->
|
||||||
<div v-if="isOwner" class="owner-actions">
|
<div v-if="isOwner" class="owner-actions">
|
||||||
<!-- 主操作:根据 status 切换 -->
|
<!-- 主操作:根据 status 切换 -->
|
||||||
<button
|
<button v-if="work.status === 'unpublished'" class="op-btn primary" :disabled="actionLoading"
|
||||||
v-if="work.status === 'unpublished'"
|
@click="handlePublish">
|
||||||
class="op-btn primary"
|
|
||||||
:disabled="actionLoading"
|
|
||||||
@click="handlePublish"
|
|
||||||
>
|
|
||||||
<send-outlined />
|
<send-outlined />
|
||||||
<span>公开发布</span>
|
<span>公开发布</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button v-else-if="work.status === 'rejected'" class="op-btn primary" :disabled="actionLoading"
|
||||||
v-else-if="work.status === 'rejected'"
|
@click="handleResubmit">
|
||||||
class="op-btn primary"
|
|
||||||
:disabled="actionLoading"
|
|
||||||
@click="handleResubmit"
|
|
||||||
>
|
|
||||||
<send-outlined />
|
<send-outlined />
|
||||||
<span>修改后重交</span>
|
<span>修改后重交</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button v-else-if="work.status === 'draft'" class="op-btn primary" @click="handleContinue">
|
||||||
v-else-if="work.status === 'draft'"
|
|
||||||
class="op-btn primary"
|
|
||||||
@click="handleContinue"
|
|
||||||
>
|
|
||||||
<edit-outlined />
|
<edit-outlined />
|
||||||
<span>继续创作</span>
|
<span>继续创作</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button v-else-if="work.status === 'pending_review'" class="op-btn outline" :disabled="actionLoading"
|
||||||
v-else-if="work.status === 'pending_review'"
|
@click="handleWithdraw">
|
||||||
class="op-btn outline"
|
|
||||||
:disabled="actionLoading"
|
|
||||||
@click="handleWithdraw"
|
|
||||||
>
|
|
||||||
<undo-outlined />
|
<undo-outlined />
|
||||||
<span>撤回审核</span>
|
<span>撤回审核</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button v-else-if="work.status === 'published'" class="op-btn outline" :disabled="actionLoading"
|
||||||
v-else-if="work.status === 'published'"
|
@click="handleUnpublish">
|
||||||
class="op-btn outline"
|
|
||||||
:disabled="actionLoading"
|
|
||||||
@click="handleUnpublish"
|
|
||||||
>
|
|
||||||
<inbox-outlined />
|
<inbox-outlined />
|
||||||
<span>下架</span>
|
<span>下架</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- 编辑信息(unpublished 状态)-->
|
<!-- 编辑信息(unpublished 状态)-->
|
||||||
<button
|
<button v-if="work.status === 'unpublished'" class="op-btn outline-soft" @click="handleEditInfo">
|
||||||
v-if="work.status === 'unpublished'"
|
|
||||||
class="op-btn outline-soft"
|
|
||||||
@click="handleEditInfo"
|
|
||||||
>
|
|
||||||
<edit-outlined />
|
<edit-outlined />
|
||||||
<span>编辑信息</span>
|
<span>编辑信息</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- 删除(所有状态)-->
|
<!-- 删除(所有状态)-->
|
||||||
<button
|
<button class="op-btn ghost-danger" :disabled="actionLoading" @click="handleDelete">
|
||||||
class="op-btn ghost-danger"
|
|
||||||
:disabled="actionLoading"
|
|
||||||
@click="handleDelete"
|
|
||||||
>
|
|
||||||
<delete-outlined />
|
<delete-outlined />
|
||||||
<span>删除</span>
|
<span>删除</span>
|
||||||
</button>
|
</button>
|
||||||
@ -188,15 +161,8 @@
|
|||||||
</a-spin>
|
</a-spin>
|
||||||
|
|
||||||
<!-- 二次确认弹窗 -->
|
<!-- 二次确认弹窗 -->
|
||||||
<a-modal
|
<a-modal v-model:open="confirmVisible" :title="confirmTitle" :ok-text="confirmOkText" cancel-text="取消"
|
||||||
v-model:open="confirmVisible"
|
:confirm-loading="actionLoading" @ok="handleConfirmOk" @cancel="handleConfirmCancel">
|
||||||
:title="confirmTitle"
|
|
||||||
:ok-text="confirmOkText"
|
|
||||||
cancel-text="取消"
|
|
||||||
:confirm-loading="actionLoading"
|
|
||||||
@ok="handleConfirmOk"
|
|
||||||
@cancel="handleConfirmCancel"
|
|
||||||
>
|
|
||||||
<p>{{ confirmContent }}</p>
|
<p>{{ confirmContent }}</p>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
@ -541,6 +507,7 @@ $accent: #ec4899;
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-btn {
|
.back-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -555,10 +522,15 @@ $accent: #ec4899;
|
|||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
:deep(.anticon) { font-size: 15px; }
|
:deep(.anticon) {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover { background: rgba($primary, 0.14); }
|
&:hover {
|
||||||
|
background: rgba($primary, 0.14);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-tag {
|
.status-tag {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
@ -568,12 +540,29 @@ $accent: #ec4899;
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
letter-spacing: 0.3px;
|
letter-spacing: 0.3px;
|
||||||
|
|
||||||
&.draft { background: rgba(107, 114, 128, 0.85); }
|
&.draft {
|
||||||
&.unpublished { background: rgba(99, 102, 241, 0.9); }
|
background: rgba(107, 114, 128, 0.85);
|
||||||
&.pending_review { background: rgba(245, 158, 11, 0.92); }
|
}
|
||||||
&.published { background: rgba(16, 185, 129, 0.92); }
|
|
||||||
&.rejected { background: rgba(239, 68, 68, 0.92); }
|
&.unpublished {
|
||||||
&.taken_down { background: rgba(107, 114, 128, 0.85); }
|
background: rgba(99, 102, 241, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pending_review {
|
||||||
|
background: rgba(245, 158, 11, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.published {
|
||||||
|
background: rgba(16, 185, 129, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rejected {
|
||||||
|
background: rgba(239, 68, 68, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.taken_down {
|
||||||
|
background: rgba(107, 114, 128, 0.85);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 拒绝原因 / 信息提示卡片 ---------- */
|
/* ---------- 拒绝原因 / 信息提示卡片 ---------- */
|
||||||
@ -585,33 +574,64 @@ $accent: #ec4899;
|
|||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reject-card {
|
.reject-card {
|
||||||
background: rgba(239, 68, 68, 0.06);
|
background: rgba(239, 68, 68, 0.06);
|
||||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.reject-icon {
|
.reject-icon {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: #ef4444;
|
color: #ef4444;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
.reject-body { flex: 1; }
|
|
||||||
.reject-title { font-size: 13px; font-weight: 700; color: #b91c1c; margin-bottom: 4px; }
|
.reject-body {
|
||||||
.reject-content { font-size: 13px; color: #4b5563; line-height: 1.6; }
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reject-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #b91c1c;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reject-content {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #4b5563;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
.info-card {
|
.info-card {
|
||||||
background: linear-gradient(135deg, rgba($primary, 0.08), rgba($accent, 0.05));
|
background: linear-gradient(135deg, rgba($primary, 0.08), rgba($accent, 0.05));
|
||||||
border: 1px solid rgba($primary, 0.15);
|
border: 1px solid rgba($primary, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-icon {
|
.info-icon {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: $primary;
|
color: $primary;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
.info-body { flex: 1; }
|
|
||||||
.info-title { font-size: 13px; font-weight: 700; color: #1e1b4b; margin-bottom: 3px; }
|
.info-body {
|
||||||
.info-desc { font-size: 12px; color: #6b7280; line-height: 1.6; }
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e1b4b;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6b7280;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- 画作原图卡片 ---------- */
|
/* ---------- 画作原图卡片 ---------- */
|
||||||
.original-card {
|
.original-card {
|
||||||
@ -625,6 +645,7 @@ $accent: #ec4899;
|
|||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
box-shadow: 0 2px 12px rgba($primary, 0.05);
|
box-shadow: 0 2px 12px rgba($primary, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.original-thumb {
|
.original-thumb {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 84px;
|
width: 84px;
|
||||||
@ -640,7 +661,10 @@ $accent: #ec4899;
|
|||||||
&:hover {
|
&:hover {
|
||||||
border-color: $primary;
|
border-color: $primary;
|
||||||
transform: scale(1.03);
|
transform: scale(1.03);
|
||||||
.zoom-hint { opacity: 1; }
|
|
||||||
|
.zoom-hint {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
@ -663,16 +687,19 @@ $accent: #ec4899;
|
|||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.original-text {
|
.original-text {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.original-title {
|
.original-title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1e1b4b;
|
color: #1e1b4b;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.original-desc {
|
.original-desc {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
@ -691,6 +718,7 @@ $accent: #ec4899;
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: zoom-out;
|
cursor: zoom-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-full-img {
|
.preview-full-img {
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
@ -698,10 +726,16 @@ $accent: #ec4899;
|
|||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4);
|
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-active,
|
.fade-enter-active,
|
||||||
.fade-leave-active { transition: opacity 0.2s; }
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
.fade-enter-from,
|
.fade-enter-from,
|
||||||
.fade-leave-to { opacity: 0; }
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- 绘本阅读器 ---------- */
|
/* ---------- 绘本阅读器 ---------- */
|
||||||
.book-reader {
|
.book-reader {
|
||||||
@ -750,7 +784,11 @@ $accent: #ec4899;
|
|||||||
|
|
||||||
.page-audio {
|
.page-audio {
|
||||||
padding: 0 20px 12px;
|
padding: 0 20px 12px;
|
||||||
.audio-player { width: 100%; height: 36px; }
|
|
||||||
|
.audio-player {
|
||||||
|
width: 100%;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-nav {
|
.page-nav {
|
||||||
@ -773,7 +811,9 @@ $accent: #ec4899;
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
|
||||||
:deep(.anticon) { font-size: 14px; }
|
:deep(.anticon) {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover:not(:disabled) {
|
&:hover:not(:disabled) {
|
||||||
border-color: $primary;
|
border-color: $primary;
|
||||||
@ -810,9 +850,21 @@ $accent: #ec4899;
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
|
||||||
.author-info { display: flex; flex-direction: column; }
|
.author-info {
|
||||||
.author-name { font-size: 14px; font-weight: 600; color: #1e1b4b; }
|
display: flex;
|
||||||
.create-time { font-size: 11px; color: #9ca3af; }
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e1b4b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-time {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
@ -863,7 +915,10 @@ $accent: #ec4899;
|
|||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
span { font-size: 13px; font-weight: 500; }
|
span {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba($primary, 0.04);
|
background: rgba($primary, 0.04);
|
||||||
@ -872,7 +927,10 @@ $accent: #ec4899;
|
|||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: $accent;
|
color: $accent;
|
||||||
&:hover { background: rgba($accent, 0.06); }
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba($accent, 0.06);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active :deep(.anticon) {
|
&.active :deep(.anticon) {
|
||||||
@ -882,9 +940,17 @@ $accent: #ec4899;
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pop {
|
@keyframes pop {
|
||||||
0% { transform: scale(1); }
|
0% {
|
||||||
50% { transform: scale(1.3); }
|
transform: scale(1);
|
||||||
100% { transform: scale(1); }
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 作者私有操作区 ---------- */
|
/* ---------- 作者私有操作区 ---------- */
|
||||||
@ -898,6 +964,7 @@ $accent: #ec4899;
|
|||||||
border: 1px solid rgba($primary, 0.06);
|
border: 1px solid rgba($primary, 0.06);
|
||||||
box-shadow: 0 2px 12px rgba($primary, 0.05);
|
box-shadow: 0 2px 12px rgba($primary, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.op-btn {
|
.op-btn {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
@ -913,10 +980,18 @@ $accent: #ec4899;
|
|||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
:deep(.anticon) { font-size: 13px; }
|
:deep(.anticon) {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
&:active { transform: scale(0.97); }
|
&:active {
|
||||||
&:disabled { opacity: 0.4; pointer-events: none; }
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.op-btn.primary {
|
.op-btn.primary {
|
||||||
@ -925,7 +1000,10 @@ $accent: #ec4899;
|
|||||||
border: none;
|
border: none;
|
||||||
box-shadow: 0 4px 14px rgba($primary, 0.32);
|
box-shadow: 0 4px 14px rgba($primary, 0.32);
|
||||||
|
|
||||||
&:hover { transform: translateY(-1px); box-shadow: 0 6px 18px rgba($primary, 0.4); }
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 6px 18px rgba($primary, 0.4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.op-btn.outline {
|
.op-btn.outline {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user