feat: 公众端多项功能增强——短信登录、作品状态优化、创作流程组件 keep-alive
后端: - 新增手机号验证码登录接口及 PublicSmsLoginDto - LeaiSync 作品同步状态阈值从 CATALOGED 调整为 DUBBED - UgcWork 实体字段微调、数据库迁移脚本修正 前端: - Login 页面支持用户名/手机号双模式登录 - public.ts 新增 loginBySms、sendSmsCode API - AI 创作流程全部视图添加 keep-alive 组件名导出 - CreatingView 生成逻辑优化 - WelcomeView 欢迎页增强 - BookReaderView、作品库等页面细节修复 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
387ee5ccfb
commit
8995e2f2e2
@ -109,8 +109,8 @@ public class LeaiSyncService implements ILeaiSyncService {
|
|||||||
work.setTitle(LeaiUtil.toString(remoteData.get("title"), "未命名作品"));
|
work.setTitle(LeaiUtil.toString(remoteData.get("title"), "未命名作品"));
|
||||||
int leaiStatus = LeaiUtil.toInt(remoteData.get("status"), LeaiCreationStatus.PENDING);
|
int leaiStatus = LeaiUtil.toInt(remoteData.get("status"), LeaiCreationStatus.PENDING);
|
||||||
work.setLeaiStatus(leaiStatus);
|
work.setLeaiStatus(leaiStatus);
|
||||||
// 本地发布状态:创作进度 >= CATALOGED 时自动设为 unpublished,否则为 draft
|
// 本地发布状态:创作进度 >= DUBBED 时自动设为 unpublished,否则为 draft
|
||||||
work.setStatus(leaiStatus >= LeaiCreationStatus.CATALOGED
|
work.setStatus(leaiStatus >= LeaiCreationStatus.DUBBED
|
||||||
? WorkPublishStatus.UNPUBLISHED.getValue()
|
? WorkPublishStatus.UNPUBLISHED.getValue()
|
||||||
: WorkPublishStatus.DRAFT.getValue());
|
: WorkPublishStatus.DRAFT.getValue());
|
||||||
work.setVisibility(Visibility.PRIVATE.getValue());
|
work.setVisibility(Visibility.PRIVATE.getValue());
|
||||||
@ -228,8 +228,8 @@ public class LeaiSyncService implements ILeaiSyncService {
|
|||||||
.lt(UgcWork::getLeaiStatus, remoteStatus)
|
.lt(UgcWork::getLeaiStatus, remoteStatus)
|
||||||
.set(UgcWork::getLeaiStatus, remoteStatus);
|
.set(UgcWork::getLeaiStatus, remoteStatus);
|
||||||
|
|
||||||
// 当 leaiStatus 推进到 CATALOGED 且当前 status 仍为 draft → 自动设 status 为 unpublished
|
// 当 leaiStatus 推进到 DUBBED 且当前 status 仍为 draft → 自动设 status 为 unpublished
|
||||||
if (remoteStatus >= LeaiCreationStatus.CATALOGED
|
if (remoteStatus >= LeaiCreationStatus.DUBBED
|
||||||
&& WorkPublishStatus.DRAFT.getValue().equals(work.getStatus())) {
|
&& WorkPublishStatus.DRAFT.getValue().equals(work.getStatus())) {
|
||||||
wrapper.set(UgcWork::getStatus, WorkPublishStatus.UNPUBLISHED.getValue());
|
wrapper.set(UgcWork::getStatus, WorkPublishStatus.UNPUBLISHED.getValue());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import com.competition.common.result.Result;
|
|||||||
import com.competition.common.util.SecurityUtil;
|
import com.competition.common.util.SecurityUtil;
|
||||||
import com.competition.modules.pub.dto.PublicLoginDto;
|
import com.competition.modules.pub.dto.PublicLoginDto;
|
||||||
import com.competition.modules.pub.dto.PublicRegisterDto;
|
import com.competition.modules.pub.dto.PublicRegisterDto;
|
||||||
|
import com.competition.modules.pub.dto.PublicSmsLoginDto;
|
||||||
import com.competition.modules.pub.dto.SendSmsCodeDto;
|
import com.competition.modules.pub.dto.SendSmsCodeDto;
|
||||||
import com.competition.modules.pub.service.PublicAuthService;
|
import com.competition.modules.pub.service.PublicAuthService;
|
||||||
import com.competition.modules.pub.service.SmsCodeService;
|
import com.competition.modules.pub.service.SmsCodeService;
|
||||||
@ -42,6 +43,14 @@ public class PublicAuthController {
|
|||||||
return Result.success(publicAuthService.login(dto));
|
return Result.success(publicAuthService.login(dto));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Public
|
||||||
|
@PostMapping("/login/sms")
|
||||||
|
@RateLimit(permits = 5, duration = 1)
|
||||||
|
@Operation(summary = "手机号验证码登录")
|
||||||
|
public Result<Map<String, Object>> loginBySms(@Valid @RequestBody PublicSmsLoginDto dto) {
|
||||||
|
return Result.success(publicAuthService.loginBySms(dto));
|
||||||
|
}
|
||||||
|
|
||||||
@Public
|
@Public
|
||||||
@PostMapping("/sms/send")
|
@PostMapping("/sms/send")
|
||||||
@RateLimit(permits = 1, duration = 60)
|
@RateLimit(permits = 1, duration = 60)
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
package com.competition.modules.pub.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "手机号验证码登录请求")
|
||||||
|
public class PublicSmsLoginDto {
|
||||||
|
|
||||||
|
@NotBlank(message = "手机号不能为空")
|
||||||
|
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||||
|
@Schema(description = "手机号")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@NotBlank(message = "短信验证码不能为空")
|
||||||
|
@Size(min = 6, max = 6, message = "验证码为6位数字")
|
||||||
|
@Schema(description = "短信验证码")
|
||||||
|
private String smsCode;
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ import com.competition.common.enums.UserSource;
|
|||||||
import com.competition.common.enums.UserType;
|
import com.competition.common.enums.UserType;
|
||||||
import com.competition.modules.pub.dto.PublicLoginDto;
|
import com.competition.modules.pub.dto.PublicLoginDto;
|
||||||
import com.competition.modules.pub.dto.PublicRegisterDto;
|
import com.competition.modules.pub.dto.PublicRegisterDto;
|
||||||
|
import com.competition.modules.pub.dto.PublicSmsLoginDto;
|
||||||
import com.competition.modules.sys.entity.SysRole;
|
import com.competition.modules.sys.entity.SysRole;
|
||||||
import com.competition.modules.sys.entity.SysTenant;
|
import com.competition.modules.sys.entity.SysTenant;
|
||||||
import com.competition.modules.sys.entity.SysUser;
|
import com.competition.modules.sys.entity.SysUser;
|
||||||
@ -160,6 +161,49 @@ public class PublicAuthService {
|
|||||||
return buildAuthResult(token, user, roles);
|
return buildAuthResult(token, user, roles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号验证码登录
|
||||||
|
*/
|
||||||
|
public Map<String, Object> loginBySms(PublicSmsLoginDto dto) {
|
||||||
|
// 校验短信验证码
|
||||||
|
smsCodeService.verifyCode(dto.getPhone(), dto.getSmsCode());
|
||||||
|
|
||||||
|
// 先在公众租户下按手机号查找用户
|
||||||
|
SysTenant publicTenant = sysTenantMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<SysTenant>().eq(SysTenant::getCode, TenantConstants.CODE_PUBLIC));
|
||||||
|
|
||||||
|
SysUser user = null;
|
||||||
|
if (publicTenant != null) {
|
||||||
|
user = sysUserMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<SysUser>()
|
||||||
|
.eq(SysUser::getPhone, dto.getPhone())
|
||||||
|
.eq(SysUser::getTenantId, publicTenant.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如未找到,在所有租户中搜索
|
||||||
|
if (user == null) {
|
||||||
|
user = sysUserMapper.selectOne(
|
||||||
|
new LambdaQueryWrapper<SysUser>()
|
||||||
|
.eq(SysUser::getPhone, dto.getPhone())
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
throw new BusinessException(404, "该手机号未注册");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CommonStatus.ENABLED.getValue().equals(user.getStatus())) {
|
||||||
|
throw new BusinessException(403, "账号已被禁用");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取角色
|
||||||
|
List<String> roles = getUserRoleCodes(user.getId());
|
||||||
|
|
||||||
|
String token = jwtUtil.generateToken(user.getId(), user.getUsername(), user.getTenantId());
|
||||||
|
log.info("手机号验证码登录成功:userId={}, phone={}", user.getId(), dto.getPhone());
|
||||||
|
return buildAuthResult(token, user, roles);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 切换到子女账号
|
* 切换到子女账号
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -45,7 +45,7 @@ public class UgcWork implements Serializable {
|
|||||||
@Schema(description = "本地发布状态: draft/unpublished/pending_review/published/rejected")
|
@Schema(description = "本地发布状态: draft/unpublished/pending_review/published/rejected")
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
@Schema(description = "乐读派创作进度: -1=FAILED, 0=DRAFT, 1=PENDING, 2=PROCESSING, 3=COMPLETED, 4=CATALOGED, 5=DUBBED")
|
@Schema(description = "乐读派创作进度: -1=FAILED, 0=INIT, 1=PENDING, 2=PROCESSING, 3=COMPLETED, 4=CATALOGED, 5=DUBBED")
|
||||||
@TableField("leai_status")
|
@TableField("leai_status")
|
||||||
private Integer leaiStatus;
|
private Integer leaiStatus;
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
-- 1. 新增 leai_status 字段
|
-- 1. 新增 leai_status 字段
|
||||||
ALTER TABLE t_ugc_work ADD COLUMN leai_status INT NOT NULL DEFAULT 0
|
ALTER TABLE t_ugc_work ADD COLUMN leai_status INT NOT NULL DEFAULT 0
|
||||||
COMMENT '乐读派创作进度: -1=FAILED, 0=DRAFT, 1=PENDING, 2=PROCESSING, 3=COMPLETED, 4=CATALOGED, 5=DUBBED';
|
COMMENT '乐读派创作进度: -1=FAILED, 0=INIT, 1=PENDING, 2=PROCESSING, 3=COMPLETED, 4=CATALOGED, 5=DUBBED';
|
||||||
|
|
||||||
-- 2. 把现有 status (INT) 值复制到 leai_status
|
-- 2. 把现有 status (INT) 值复制到 leai_status
|
||||||
UPDATE t_ugc_work SET leai_status = status;
|
UPDATE t_ugc_work SET leai_status = status;
|
||||||
|
|||||||
@ -18,7 +18,7 @@ UPDATE t_ugc_work SET status = '-2' WHERE status = 'taken_down';
|
|||||||
UPDATE t_ugc_work SET status = '0' WHERE status NOT REGEXP '^-?[0-9]+$';
|
UPDATE t_ugc_work SET status = '0' WHERE status NOT REGEXP '^-?[0-9]+$';
|
||||||
|
|
||||||
ALTER TABLE t_ugc_work MODIFY COLUMN status INT NOT NULL DEFAULT 0
|
ALTER TABLE t_ugc_work MODIFY COLUMN status INT NOT NULL DEFAULT 0
|
||||||
COMMENT '创作状态:-1=FAILED, 0=DRAFT, 1=PENDING, 2=PROCESSING, 3=COMPLETED, 4=CATALOGED, 5=DUBBED';
|
COMMENT '创作状态:-1=FAILED, 0=INIT, 1=PENDING, 2=PROCESSING, 3=COMPLETED, 4=CATALOGED, 5=DUBBED';
|
||||||
|
|
||||||
-- 2. 新增乐读派关联字段
|
-- 2. 新增乐读派关联字段
|
||||||
ALTER TABLE t_ugc_work ADD COLUMN remote_work_id VARCHAR(64) DEFAULT NULL COMMENT '乐读派远程作品ID' AFTER user_id;
|
ALTER TABLE t_ugc_work ADD COLUMN remote_work_id VARCHAR(64) DEFAULT NULL COMMENT '乐读派远程作品ID' AFTER user_id;
|
||||||
|
|||||||
@ -46,8 +46,8 @@ draft / pending_review / published / rejected
|
|||||||
|
|
||||||
引入新的中间状态 **`unpublished`(未发布)**,明确语义边界:
|
引入新的中间状态 **`unpublished`(未发布)**,明确语义边界:
|
||||||
|
|
||||||
- **草稿(draft)** = 作品技术上还没完成(生成失败 / 还在生成 / 没保存编目)
|
- **草稿(draft)** = 作品技术上还没完成(生成失败 / 还在生成 / 没完成配音)
|
||||||
- **未发布(unpublished)** = 作品技术上完整(编目已保存),但用户没主动公开
|
- **未发布(unpublished)** = 作品技术上完整(配音已完成),但用户没主动公开
|
||||||
- **审核中(pending_review)** = 用户主动提交审核
|
- **审核中(pending_review)** = 用户主动提交审核
|
||||||
- **已发布(published)** = 审核通过,发现页可见
|
- **已发布(published)** = 审核通过,发现页可见
|
||||||
- **被拒绝(rejected)** = 审核未通过,可改后重新提交
|
- **被拒绝(rejected)** = 审核未通过,可改后重新提交
|
||||||
@ -70,16 +70,16 @@ draft / pending_review / published / rejected
|
|||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌───────────────┐
|
┌───────────────┐
|
||||||
│ DRAFT │ 半成品:还在生成或未保存编目
|
│ DRAFT │ 半成品:还在生成或未完成配音
|
||||||
│ 草稿 │
|
│ 草稿 │
|
||||||
└───────┬───────┘
|
└───────┬───────┘
|
||||||
│
|
│
|
||||||
│ 用户在 EditInfoView 点
|
│ 用户在 EditInfoView 点
|
||||||
│ 保存 / 去配音 / 立即发布
|
│ 保存 / 去配音 / 立即发布
|
||||||
│ 任意按钮 = 编目完成
|
│ 任意按钮 = 配音完成
|
||||||
▼
|
▼
|
||||||
┌───────────────┐
|
┌───────────────┐
|
||||||
│ UNPUBLISHED │ 成品私有:已编目,可补配音,可发布
|
│ UNPUBLISHED │ 成品私有:已配音,可发布
|
||||||
│ 未发布 │◀──────────────┐
|
│ 未发布 │◀──────────────┐
|
||||||
└───────┬───────┘ │
|
└───────┬───────┘ │
|
||||||
│ │
|
│ │
|
||||||
@ -111,7 +111,7 @@ draft / pending_review / published / rejected
|
|||||||
| 起始状态 | 触发 | 目标状态 | 触发方 | 接口 |
|
| 起始状态 | 触发 | 目标状态 | 触发方 | 接口 |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| (无) | 创建作品 | DRAFT | 系统 | leai 创作流程内部 |
|
| (无) | 创建作品 | DRAFT | 系统 | leai 创作流程内部 |
|
||||||
| DRAFT | 编目完成(leai status → CATALOGED) | UNPUBLISHED | 系统(webhook 同步) | LeaiSyncService |
|
| DRAFT | 配音完成(leai status → DUBBED) | UNPUBLISHED | 系统(webhook 同步) | LeaiSyncService |
|
||||||
| UNPUBLISHED | 用户点「公开发布」 | PENDING_REVIEW | 用户 | `POST /public/works/{id}/publish` |
|
| UNPUBLISHED | 用户点「公开发布」 | PENDING_REVIEW | 用户 | `POST /public/works/{id}/publish` |
|
||||||
| UNPUBLISHED | 用户补充配音/编辑 | UNPUBLISHED | 用户 | 更新内容接口 |
|
| UNPUBLISHED | 用户补充配音/编辑 | UNPUBLISHED | 用户 | 更新内容接口 |
|
||||||
| PENDING_REVIEW | 审核通过 | PUBLISHED | 超管 | `POST /content-review/works/{id}/approve` |
|
| PENDING_REVIEW | 审核通过 | PUBLISHED | 超管 | `POST /content-review/works/{id}/approve` |
|
||||||
@ -218,7 +218,7 @@ taken_down → -2
|
|||||||
-- 改为:
|
-- 改为:
|
||||||
ALTER TABLE t_ugc_work
|
ALTER TABLE t_ugc_work
|
||||||
ADD COLUMN leai_status INT NOT NULL DEFAULT 0
|
ADD COLUMN leai_status INT NOT NULL DEFAULT 0
|
||||||
COMMENT 'leai 创作进度: -1=FAILED, 0=DRAFT, 1=PENDING, 2=PROCESSING, 3=COMPLETED, 4=CATALOGED, 5=DUBBED';
|
COMMENT 'leai 创作进度: -1=FAILED, 0=INIT, 1=PENDING, 2=PROCESSING, 3=COMPLETED, 4=CATALOGED, 5=DUBBED';
|
||||||
|
|
||||||
-- status 字段重定义为本地发布状态(VARCHAR 跟前端对齐)
|
-- status 字段重定义为本地发布状态(VARCHAR 跟前端对齐)
|
||||||
ALTER TABLE t_ugc_work
|
ALTER TABLE t_ugc_work
|
||||||
@ -285,7 +285,7 @@ CREATE INDEX idx_ugc_work_leai_status ON t_ugc_work(leai_status);
|
|||||||
- 加 `leaiStatus: Integer`(保持 leai 进度)
|
- 加 `leaiStatus: Integer`(保持 leai 进度)
|
||||||
- 改 `status: String`(本地发布状态,对应新枚举)
|
- 改 `status: String`(本地发布状态,对应新枚举)
|
||||||
- [ ] **新建 migration `V9__split_work_status.sql`**(见 4.2)
|
- [ ] **新建 migration `V9__split_work_status.sql`**(见 4.2)
|
||||||
- [ ] **`LeaiSyncService.updateStatusForward`**:当 leai status 推进到 CATALOGED 时,自动把本地 status 从 draft 升到 unpublished(如果当前不是更高级状态)
|
- [ ] **`LeaiSyncService.updateStatusForward`**:当 leai status 推进到 DUBBED 时,自动把本地 status 从 draft 升到 unpublished(如果当前不是更高级状态)
|
||||||
- [ ] **`PublicUserWorkService.publish()`**:检查作品 `status == 'unpublished'`,改为 `unpublished → pending_review`;同时支持 `rejected → pending_review`
|
- [ ] **`PublicUserWorkService.publish()`**:检查作品 `status == 'unpublished'`,改为 `unpublished → pending_review`;同时支持 `rejected → pending_review`
|
||||||
- [ ] **新增 `POST /public/works/{id}/unpublish`**:用户主动下架,`published → unpublished`
|
- [ ] **新增 `POST /public/works/{id}/unpublish`**:用户主动下架,`published → unpublished`
|
||||||
- [ ] **新增 `POST /public/works/{id}/withdraw`**:用户撤回审核,`pending_review → unpublished`
|
- [ ] **新增 `POST /public/works/{id}/withdraw`**:用户撤回审核,`pending_review → unpublished`
|
||||||
|
|||||||
@ -48,7 +48,8 @@ publicApi.interceptors.response.use(
|
|||||||
// 检查业务状态码,非 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) {
|
||||||
const error: any = new Error(resData.message || "请求失败")
|
// 兼容后端 Result.message 和乐读派原始响应的 msg 字段
|
||||||
|
const error: any = new Error(resData.message || resData.msg || "请求失败")
|
||||||
error.response = { data: resData }
|
error.response = { data: resData }
|
||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
@ -86,6 +87,11 @@ export interface PublicLoginParams {
|
|||||||
password: string
|
password: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PublicSmsLoginParams {
|
||||||
|
phone: string
|
||||||
|
smsCode: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface PublicUser {
|
export interface PublicUser {
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
@ -116,6 +122,10 @@ export const publicAuthApi = {
|
|||||||
login: (data: PublicLoginParams): Promise<LoginResponse> =>
|
login: (data: PublicLoginParams): Promise<LoginResponse> =>
|
||||||
publicApi.post("/public/auth/login", data),
|
publicApi.post("/public/auth/login", data),
|
||||||
|
|
||||||
|
/** 手机号验证码登录 */
|
||||||
|
loginBySms: (data: PublicSmsLoginParams): Promise<LoginResponse> =>
|
||||||
|
publicApi.post("/public/auth/login/sms", data),
|
||||||
|
|
||||||
/** 发送短信验证码 */
|
/** 发送短信验证码 */
|
||||||
sendSmsCode: (phone: string): Promise<void> =>
|
sendSmsCode: (phone: string): Promise<void> =>
|
||||||
publicApi.post("/public/auth/sms/send", { phone }),
|
publicApi.post("/public/auth/sms/send", { phone }),
|
||||||
@ -395,8 +405,8 @@ export const publicInteractionApi = {
|
|||||||
* published → unpublished(下架)
|
* published → unpublished(下架)
|
||||||
* rejected → pending_review(改后重交)
|
* rejected → pending_review(改后重交)
|
||||||
*
|
*
|
||||||
* - draft 草稿:作品技术上还没完成(生成失败 / 还在生成 / 没保存编目)
|
* - draft 草稿:作品技术上还没完成(生成失败 / 还在生成 / 没完成配音)
|
||||||
* - unpublished 未发布:作品技术上完整(编目已保存),但用户没主动公开
|
* - unpublished 未发布:作品技术上完整(配音已完成),但用户没主动公开
|
||||||
* - pending_review 审核中:用户主动提交审核
|
* - pending_review 审核中:用户主动提交审核
|
||||||
* - published 已发布:审核通过,发现页可见
|
* - published 已发布:审核通过,发现页可见
|
||||||
* - rejected 被拒绝:审核未通过
|
* - rejected 被拒绝:审核未通过
|
||||||
|
|||||||
@ -9,12 +9,12 @@
|
|||||||
|
|
||||||
<a-form
|
<a-form
|
||||||
:model="form"
|
:model="form"
|
||||||
:rules="rules"
|
:rules="currentRules"
|
||||||
@finish="handleSubmit"
|
@finish="handleSubmit"
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
class="auth-form"
|
class="auth-form"
|
||||||
>
|
>
|
||||||
<a-form-item v-if="isRegister" name="nickname" label="昵称">
|
<a-form-item v-if="showNicknameField" name="nickname" label="昵称">
|
||||||
<a-input
|
<a-input
|
||||||
v-model:value="form.nickname"
|
v-model:value="form.nickname"
|
||||||
placeholder="给自己取个名字吧"
|
placeholder="给自己取个名字吧"
|
||||||
@ -22,7 +22,7 @@
|
|||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item v-if="isRegister" name="phone" label="手机号">
|
<a-form-item v-if="showPhoneField" name="phone" label="手机号">
|
||||||
<a-input
|
<a-input
|
||||||
v-model:value="form.phone"
|
v-model:value="form.phone"
|
||||||
placeholder="请输入手机号"
|
placeholder="请输入手机号"
|
||||||
@ -31,7 +31,7 @@
|
|||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item v-if="isRegister" name="smsCode" label="验证码">
|
<a-form-item v-if="showSmsCodeField" name="smsCode" label="验证码">
|
||||||
<div style="display: flex; gap: 8px">
|
<div style="display: flex; gap: 8px">
|
||||||
<a-input
|
<a-input
|
||||||
v-model:value="form.smsCode"
|
v-model:value="form.smsCode"
|
||||||
@ -52,7 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item name="username" label="用户名">
|
<a-form-item v-if="showUsernameField" name="username" label="用户名">
|
||||||
<a-input
|
<a-input
|
||||||
v-model:value="form.username"
|
v-model:value="form.username"
|
||||||
placeholder="请输入用户名"
|
placeholder="请输入用户名"
|
||||||
@ -60,7 +60,7 @@
|
|||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item name="password" label="密码">
|
<a-form-item v-if="showPasswordField" name="password" label="密码">
|
||||||
<a-input-password
|
<a-input-password
|
||||||
v-model:value="form.password"
|
v-model:value="form.password"
|
||||||
placeholder="请输入密码"
|
placeholder="请输入密码"
|
||||||
@ -90,14 +90,20 @@
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
|
<div v-if="!isRegister" class="login-method-switch">
|
||||||
|
<a @click="toggleLoginMethod">
|
||||||
|
{{ loginMethod === 'password' ? '使用手机号登录' : '使用账号密码登录' }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="auth-switch">
|
<div class="auth-switch">
|
||||||
<template v-if="isRegister">
|
<template v-if="isRegister">
|
||||||
已有账号?
|
已有账号?
|
||||||
<a @click="isRegister = false">去登录</a>
|
<a @click="switchToLogin">去登录</a>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
还没有账号?
|
还没有账号?
|
||||||
<a @click="isRegister = true">立即注册</a>
|
<a @click="switchToRegister">立即注册</a>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -124,10 +130,11 @@ const router = useRouter()
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const isRegister = ref(false)
|
const isRegister = ref(false)
|
||||||
|
const loginMethod = ref<'password' | 'sms'>('password')
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
username: "demo",
|
username: import.meta.env.DEV ? "demo" : "",
|
||||||
password: "demo123456",
|
password: import.meta.env.DEV ? "demo123456" : "",
|
||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
nickname: "",
|
nickname: "",
|
||||||
phone: import.meta.env.DEV ? "13800138000" : "",
|
phone: import.meta.env.DEV ? "13800138000" : "",
|
||||||
@ -141,35 +148,15 @@ let smsTimer: ReturnType<typeof setInterval> | null = null
|
|||||||
|
|
||||||
const isPhoneValid = computed(() => /^1[3-9]\d{9}$/.test(form.phone))
|
const isPhoneValid = computed(() => /^1[3-9]\d{9}$/.test(form.phone))
|
||||||
|
|
||||||
const handleSendSms = async () => {
|
// 控制字段显示的 computed
|
||||||
if (!isPhoneValid.value || smsCountdown.value > 0) return
|
const showNicknameField = computed(() => isRegister.value)
|
||||||
smsSending.value = true
|
const showPhoneField = computed(() => isRegister.value || (!isRegister.value && loginMethod.value === 'sms'))
|
||||||
try {
|
const showSmsCodeField = computed(() => isRegister.value || (!isRegister.value && loginMethod.value === 'sms'))
|
||||||
await publicAuthApi.sendSmsCode(form.phone)
|
const showUsernameField = computed(() => !isRegister.value && loginMethod.value === 'password')
|
||||||
message.success("验证码已发送")
|
const showPasswordField = computed(() => !isRegister.value && loginMethod.value === 'password')
|
||||||
// 开始 60 秒倒计时
|
|
||||||
smsCountdown.value = 60
|
|
||||||
smsTimer = setInterval(() => {
|
|
||||||
smsCountdown.value--
|
|
||||||
if (smsCountdown.value <= 0) {
|
|
||||||
if (smsTimer) clearInterval(smsTimer)
|
|
||||||
smsTimer = null
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
} catch (error: any) {
|
|
||||||
const msg = error?.response?.data?.message || "验证码发送失败"
|
|
||||||
message.error(msg)
|
|
||||||
} finally {
|
|
||||||
smsSending.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件销毁时清理定时器
|
// 基础校验规则
|
||||||
onUnmounted(() => {
|
const baseRules: Record<string, Rule[]> = {
|
||||||
if (smsTimer) clearInterval(smsTimer)
|
|
||||||
})
|
|
||||||
|
|
||||||
const rules: Record<string, Rule[]> = {
|
|
||||||
username: [
|
username: [
|
||||||
{ required: true, message: "请输入用户名", trigger: "blur" },
|
{ required: true, message: "请输入用户名", trigger: "blur" },
|
||||||
{ min: 4, message: "用户名至少4个字符", trigger: "blur" },
|
{ min: 4, message: "用户名至少4个字符", trigger: "blur" },
|
||||||
@ -205,6 +192,71 @@ const rules: Record<string, Rule[]> = {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据当前模式动态生成校验规则
|
||||||
|
const currentRules = computed(() => {
|
||||||
|
const rules: Record<string, Rule[]> = {}
|
||||||
|
if (isRegister.value) {
|
||||||
|
// 注册模式:昵称、手机号、验证码、用户名、密码、确认密码
|
||||||
|
rules.nickname = baseRules.nickname
|
||||||
|
rules.phone = baseRules.phone
|
||||||
|
rules.smsCode = baseRules.smsCode
|
||||||
|
rules.username = baseRules.username
|
||||||
|
rules.password = baseRules.password
|
||||||
|
rules.confirmPassword = baseRules.confirmPassword
|
||||||
|
} else if (loginMethod.value === 'password') {
|
||||||
|
// 密码登录模式:用户名、密码
|
||||||
|
rules.username = baseRules.username
|
||||||
|
rules.password = baseRules.password
|
||||||
|
} else {
|
||||||
|
// 验证码登录模式:手机号、验证码
|
||||||
|
rules.phone = baseRules.phone
|
||||||
|
rules.smsCode = baseRules.smsCode
|
||||||
|
}
|
||||||
|
return rules
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSendSms = async () => {
|
||||||
|
if (!isPhoneValid.value || smsCountdown.value > 0) return
|
||||||
|
smsSending.value = true
|
||||||
|
try {
|
||||||
|
await publicAuthApi.sendSmsCode(form.phone)
|
||||||
|
message.success("验证码已发送")
|
||||||
|
// 开始 60 秒倒计时
|
||||||
|
smsCountdown.value = 60
|
||||||
|
smsTimer = setInterval(() => {
|
||||||
|
smsCountdown.value--
|
||||||
|
if (smsCountdown.value <= 0) {
|
||||||
|
if (smsTimer) clearInterval(smsTimer)
|
||||||
|
smsTimer = null
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
} catch (error: any) {
|
||||||
|
const msg = error?.response?.data?.message || "验证码发送失败"
|
||||||
|
message.error(msg)
|
||||||
|
} finally {
|
||||||
|
smsSending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件销毁时清理定时器
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (smsTimer) clearInterval(smsTimer)
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleLoginMethod = () => {
|
||||||
|
loginMethod.value = loginMethod.value === 'password' ? 'sms' : 'password'
|
||||||
|
}
|
||||||
|
|
||||||
|
const switchToRegister = () => {
|
||||||
|
isRegister.value = true
|
||||||
|
loginMethod.value = 'password'
|
||||||
|
}
|
||||||
|
|
||||||
|
const switchToLogin = () => {
|
||||||
|
isRegister.value = false
|
||||||
|
loginMethod.value = 'password'
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@ -218,6 +270,12 @@ const handleSubmit = async () => {
|
|||||||
smsCode: form.smsCode,
|
smsCode: form.smsCode,
|
||||||
})
|
})
|
||||||
message.success("注册成功")
|
message.success("注册成功")
|
||||||
|
} else if (loginMethod.value === 'sms') {
|
||||||
|
result = await publicAuthApi.loginBySms({
|
||||||
|
phone: form.phone,
|
||||||
|
smsCode: form.smsCode,
|
||||||
|
})
|
||||||
|
message.success("登录成功")
|
||||||
} else {
|
} else {
|
||||||
result = await publicAuthApi.login({
|
result = await publicAuthApi.login({
|
||||||
username: form.username,
|
username: form.username,
|
||||||
@ -349,8 +407,24 @@ $primary: #6366f1;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-method-switch {
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #9ca3af;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $primary;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #ec4899;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.auth-switch {
|
.auth-switch {
|
||||||
margin-top: 20px;
|
margin-top: 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export default { name: 'BookReaderView' }
|
||||||
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="reader-page page-fullscreen" @touchstart="onTouchStart" @touchend="onTouchEnd">
|
<div class="reader-page page-fullscreen" @touchstart="onTouchStart" @touchend="onTouchEnd">
|
||||||
<!-- 顶栏 -->
|
<!-- 顶栏 -->
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export default { name: 'CharactersView' }
|
||||||
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="char-page page-fullscreen">
|
<div class="char-page page-fullscreen">
|
||||||
<PageHeader title="画作角色" :subtitle="subtitle" :step="1" />
|
<PageHeader title="画作角色" :subtitle="subtitle" :step="1" />
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export default { name: 'CreatingView' }
|
||||||
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="creating-page">
|
<div class="creating-page">
|
||||||
<!-- 开发模式:状态切换 -->
|
<!-- 开发模式:状态切换 -->
|
||||||
@ -63,7 +66,7 @@
|
|||||||
<button v-if="store.workId" class="btn-primary error-btn" @click="resumePolling">
|
<button v-if="store.workId" class="btn-primary error-btn" @click="resumePolling">
|
||||||
恢复查询进度
|
恢复查询进度
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-primary error-btn" :class="{ 'btn-outline': !!store.workId }" @click="retry">
|
<button v-if="!isQuotaError" class="btn-primary error-btn" :class="{ 'btn-outline': !!store.workId }" @click="retry">
|
||||||
重新创作
|
重新创作
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -91,7 +94,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
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 {
|
||||||
@ -112,6 +115,11 @@ const progress = ref(0)
|
|||||||
const stage = ref('准备中…')
|
const stage = ref('准备中…')
|
||||||
const dots = ref('')
|
const dots = ref('')
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
/** 额度/次数限制类错误,不应显示"重新创作"按钮 */
|
||||||
|
const isQuotaError = computed(() => {
|
||||||
|
const msg = error.value
|
||||||
|
return msg.includes('次数已达上限') || msg.includes('额度不足')
|
||||||
|
})
|
||||||
const networkWarn = ref(false)
|
const networkWarn = ref(false)
|
||||||
const currentTipIdx = ref(0)
|
const currentTipIdx = ref(0)
|
||||||
const creatingTips = [
|
const creatingTips = [
|
||||||
@ -137,6 +145,8 @@ const MAX_POLL_ERRORS = 15
|
|||||||
// 错误消息脱敏
|
// 错误消息脱敏
|
||||||
function sanitizeError(msg: string | undefined): string {
|
function sanitizeError(msg: string | undefined): string {
|
||||||
if (!msg) return '创作遇到问题,请重新尝试'
|
if (!msg) return '创作遇到问题,请重新尝试'
|
||||||
|
if (msg.includes('调用次数已达上限') || msg.includes('30004')) return '今日创作次数已达上限,请明天再试'
|
||||||
|
if (msg.includes('额度') || msg.includes('30003')) return '创作额度不足,请联系管理员'
|
||||||
if (msg.includes('400') || msg.includes('status code')) return '创作请求异常,请返回重新操作'
|
if (msg.includes('400') || msg.includes('status code')) return '创作请求异常,请返回重新操作'
|
||||||
if (msg.includes('火山引擎') || msg.includes('VolcEngine')) return 'AI 服务暂时繁忙,请稍后重试'
|
if (msg.includes('火山引擎') || msg.includes('VolcEngine')) return 'AI 服务暂时繁忙,请稍后重试'
|
||||||
if (msg.includes('额度')) return msg
|
if (msg.includes('额度')) return msg
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export default { name: 'DubbingView' }
|
||||||
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="dubbing-page page-fullscreen">
|
<div class="dubbing-page page-fullscreen">
|
||||||
<PageHeader title="绘本配音" subtitle="为每一页添加 AI 配音或亲自录音" :showBack="true" />
|
<PageHeader title="绘本配音" subtitle="为每一页添加 AI 配音或亲自录音" :showBack="true" />
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
<script>
|
||||||
|
export default { name: 'EditInfoView' }
|
||||||
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="edit-page page-fullscreen">
|
<div class="edit-page page-fullscreen">
|
||||||
<PageHeader title="编辑绘本信息" subtitle="完善信息,作品将进入「未发布」可随时发布" :showBack="true" />
|
<PageHeader title="编辑绘本信息" subtitle="完善信息,作品将进入「未发布」可随时发布" :showBack="true" />
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export default { name: 'PreviewView' }
|
||||||
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="preview-page page-fullscreen">
|
<div class="preview-page page-fullscreen">
|
||||||
<!-- 顶部 -->
|
<!-- 顶部 -->
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export default { name: 'SaveSuccessView' }
|
||||||
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="success-page">
|
<div class="success-page">
|
||||||
<!-- 撒花装饰 -->
|
<!-- 撒花装饰 -->
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export default { name: 'StoryInputView' }
|
||||||
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="story-page page-fullscreen">
|
<div class="story-page page-fullscreen">
|
||||||
<PageHeader title="编写故事" subtitle="告诉 AI 你想要什么样的故事" :step="2" />
|
<PageHeader title="编写故事" subtitle="告诉 AI 你想要什么样的故事" :step="2" />
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export default { name: 'UploadView' }
|
||||||
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="upload-page page-fullscreen">
|
<div class="upload-page page-fullscreen">
|
||||||
<PageHeader title="上传作品" subtitle="上传你的画作,AI 自动识别角色" :step="0" />
|
<PageHeader title="上传作品" subtitle="上传你的画作,AI 自动识别角色" :step="0" />
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export default { name: 'WelcomeView' }
|
||||||
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="welcome-page">
|
<div class="welcome-page">
|
||||||
<!-- Hero -->
|
<!-- Hero -->
|
||||||
@ -18,18 +21,95 @@
|
|||||||
<section class="card steps-card">
|
<section class="card steps-card">
|
||||||
<h2 class="card-title">创作流程</h2>
|
<h2 class="card-title">创作流程</h2>
|
||||||
<div class="steps">
|
<div class="steps">
|
||||||
<div v-for="(s, i) in steps" :key="i" class="step">
|
<div class="step">
|
||||||
<div class="step-left">
|
<div class="step-left">
|
||||||
<div class="step-num">{{ i + 1 }}</div>
|
<div class="step-num">1</div>
|
||||||
<div v-if="i < steps.length - 1" class="step-line" />
|
<div class="step-line" />
|
||||||
</div>
|
</div>
|
||||||
<div class="step-right">
|
<div class="step-right">
|
||||||
<div class="step-head">
|
<div class="step-head">
|
||||||
<component :is="s.icon" class="step-icon" />
|
<camera-outlined class="step-icon" />
|
||||||
<span class="step-title">{{ s.title }}</span>
|
<span class="step-title">拍照上传</span>
|
||||||
<span v-if="s.optional" class="step-tag">可选</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="step-desc">{{ s.desc }}</div>
|
<div class="step-desc">拍下你的画作</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-left">
|
||||||
|
<div class="step-num">2</div>
|
||||||
|
<div class="step-line" />
|
||||||
|
</div>
|
||||||
|
<div class="step-right">
|
||||||
|
<div class="step-head">
|
||||||
|
<smile-outlined class="step-icon" />
|
||||||
|
<span class="step-title">角色提取</span>
|
||||||
|
</div>
|
||||||
|
<div class="step-desc">AI 智能识别画中角色</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-left">
|
||||||
|
<div class="step-num">3</div>
|
||||||
|
<div class="step-line" />
|
||||||
|
</div>
|
||||||
|
<div class="step-right">
|
||||||
|
<div class="step-head">
|
||||||
|
<edit-outlined class="step-icon" />
|
||||||
|
<span class="step-title">编排故事</span>
|
||||||
|
</div>
|
||||||
|
<div class="step-desc">起书名、定主角、填故事要素</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-left">
|
||||||
|
<div class="step-num">4</div>
|
||||||
|
<div class="step-line" />
|
||||||
|
</div>
|
||||||
|
<div class="step-right">
|
||||||
|
<div class="step-head">
|
||||||
|
<bg-colors-outlined class="step-icon" />
|
||||||
|
<span class="step-title">选择画风</span>
|
||||||
|
</div>
|
||||||
|
<div class="step-desc">水墨、3D 等多种风格</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-left">
|
||||||
|
<div class="step-num">5</div>
|
||||||
|
<div class="step-line" />
|
||||||
|
</div>
|
||||||
|
<div class="step-right">
|
||||||
|
<div class="step-head">
|
||||||
|
<thunderbolt-outlined class="step-icon" />
|
||||||
|
<span class="step-title">AI 生成</span>
|
||||||
|
</div>
|
||||||
|
<div class="step-desc">一键生成完整绘本</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-left">
|
||||||
|
<div class="step-num">6</div>
|
||||||
|
<div class="step-line" />
|
||||||
|
</div>
|
||||||
|
<div class="step-right">
|
||||||
|
<div class="step-head">
|
||||||
|
<eye-outlined class="step-icon" />
|
||||||
|
<span class="step-title">预览编目</span>
|
||||||
|
</div>
|
||||||
|
<div class="step-desc">浏览成果,补充作者署名</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-left">
|
||||||
|
<div class="step-num">7</div>
|
||||||
|
</div>
|
||||||
|
<div class="step-right">
|
||||||
|
<div class="step-head">
|
||||||
|
<sound-outlined class="step-icon" />
|
||||||
|
<span class="step-title">配音发布</span>
|
||||||
|
<span class="step-tag">可选</span>
|
||||||
|
</div>
|
||||||
|
<div class="step-desc">AI 配音或亲自录音</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -66,16 +146,6 @@ import { useAicreateStore } from '@/stores/aicreate'
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const store = useAicreateStore()
|
const store = useAicreateStore()
|
||||||
|
|
||||||
const steps = [
|
|
||||||
{ icon: CameraOutlined, title: '拍照上传', desc: '拍下你的画作' },
|
|
||||||
{ icon: SmileOutlined, title: '角色提取', desc: 'AI 智能识别画中角色' },
|
|
||||||
{ icon: EditOutlined, title: '编排故事', desc: '起书名、定主角、填故事要素' },
|
|
||||||
{ icon: BgColorsOutlined, title: '选择画风', desc: '水墨、3D 等多种风格' },
|
|
||||||
{ icon: ThunderboltOutlined, title: 'AI 生成', desc: '一键生成完整绘本' },
|
|
||||||
{ icon: EyeOutlined, title: '预览编目', desc: '浏览成果,补充作者署名' },
|
|
||||||
{ icon: SoundOutlined, title: '配音发布', desc: 'AI 配音或亲自录音', optional: true },
|
|
||||||
]
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 检查恢复状态
|
// 检查恢复状态
|
||||||
const recovery = store.restoreRecoveryState()
|
const recovery = store.restoreRecoveryState()
|
||||||
|
|||||||
@ -105,8 +105,10 @@ import {
|
|||||||
EditOutlined,
|
EditOutlined,
|
||||||
} from "@ant-design/icons-vue"
|
} from "@ant-design/icons-vue"
|
||||||
import { publicProfileApi, publicMineApi } from "@/api/public"
|
import { publicProfileApi, publicMineApi } from "@/api/public"
|
||||||
|
import { useAicreateStore } from "@/stores/aicreate"
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const aicreateStore = useAicreateStore()
|
||||||
const user = ref<any>(null)
|
const user = ref<any>(null)
|
||||||
const showEditModal = ref(false)
|
const showEditModal = ref(false)
|
||||||
const editLoading = ref(false)
|
const editLoading = ref(false)
|
||||||
@ -191,6 +193,9 @@ const handleLogout = () => {
|
|||||||
localStorage.removeItem("public_user")
|
localStorage.removeItem("public_user")
|
||||||
localStorage.removeItem("parent_token_backup")
|
localStorage.removeItem("parent_token_backup")
|
||||||
localStorage.removeItem("parent_user_backup")
|
localStorage.removeItem("parent_user_backup")
|
||||||
|
// 重置创作页状态
|
||||||
|
aicreateStore.reset()
|
||||||
|
aicreateStore.clearSession()
|
||||||
router.push("/p/login")
|
router.push("/p/login")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user