From 8995e2f2e2467b0f9171bcaab72bc9ab0bedc04c Mon Sep 17 00:00:00 2001 From: En Date: Fri, 10 Apr 2026 11:19:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=85=AC=E4=BC=97=E7=AB=AF=E5=A4=9A?= =?UTF-8?q?=E9=A1=B9=E5=8A=9F=E8=83=BD=E5=A2=9E=E5=BC=BA=E2=80=94=E2=80=94?= =?UTF-8?q?=E7=9F=AD=E4=BF=A1=E7=99=BB=E5=BD=95=E3=80=81=E4=BD=9C=E5=93=81?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E4=BC=98=E5=8C=96=E3=80=81=E5=88=9B=E4=BD=9C?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E7=BB=84=E4=BB=B6=20keep-alive?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端: - 新增手机号验证码登录接口及 PublicSmsLoginDto - LeaiSync 作品同步状态阈值从 CATALOGED 调整为 DUBBED - UgcWork 实体字段微调、数据库迁移脚本修正 前端: - Login 页面支持用户名/手机号双模式登录 - public.ts 新增 loginBySms、sendSmsCode API - AI 创作流程全部视图添加 keep-alive 组件名导出 - CreatingView 生成逻辑优化 - WelcomeView 欢迎页增强 - BookReaderView、作品库等页面细节修复 Co-Authored-By: Claude Opus 4.6 --- .../modules/leai/service/LeaiSyncService.java | 8 +- .../pub/controller/PublicAuthController.java | 9 ++ .../modules/pub/dto/PublicSmsLoginDto.java | 22 +++ .../pub/service/PublicAuthService.java | 44 +++++ .../modules/ugc/entity/UgcWork.java | 2 +- .../db/migration/V17__split_work_status.sql | 2 +- .../db/migration/V5__leai_integration.sql | 2 +- .../design/public/ugc-work-status-redesign.md | 16 +- frontend/src/api/public.ts | 16 +- frontend/src/views/public/Login.vue | 152 +++++++++++++----- .../public/create/views/BookReaderView.vue | 3 + .../public/create/views/CharactersView.vue | 3 + .../public/create/views/CreatingView.vue | 14 +- .../views/public/create/views/DubbingView.vue | 3 + .../public/create/views/EditInfoView.vue | 3 + .../views/public/create/views/PreviewView.vue | 3 + .../public/create/views/SaveSuccessView.vue | 3 + .../public/create/views/StoryInputView.vue | 3 + .../views/public/create/views/UploadView.vue | 3 + .../views/public/create/views/WelcomeView.vue | 104 ++++++++++-- frontend/src/views/public/mine/Index.vue | 5 + 21 files changed, 344 insertions(+), 76 deletions(-) create mode 100644 backend-java/src/main/java/com/competition/modules/pub/dto/PublicSmsLoginDto.java diff --git a/backend-java/src/main/java/com/competition/modules/leai/service/LeaiSyncService.java b/backend-java/src/main/java/com/competition/modules/leai/service/LeaiSyncService.java index f0a83cd..213d0e1 100644 --- a/backend-java/src/main/java/com/competition/modules/leai/service/LeaiSyncService.java +++ b/backend-java/src/main/java/com/competition/modules/leai/service/LeaiSyncService.java @@ -109,8 +109,8 @@ public class LeaiSyncService implements ILeaiSyncService { work.setTitle(LeaiUtil.toString(remoteData.get("title"), "未命名作品")); int leaiStatus = LeaiUtil.toInt(remoteData.get("status"), LeaiCreationStatus.PENDING); work.setLeaiStatus(leaiStatus); - // 本地发布状态:创作进度 >= CATALOGED 时自动设为 unpublished,否则为 draft - work.setStatus(leaiStatus >= LeaiCreationStatus.CATALOGED + // 本地发布状态:创作进度 >= DUBBED 时自动设为 unpublished,否则为 draft + work.setStatus(leaiStatus >= LeaiCreationStatus.DUBBED ? WorkPublishStatus.UNPUBLISHED.getValue() : WorkPublishStatus.DRAFT.getValue()); work.setVisibility(Visibility.PRIVATE.getValue()); @@ -228,8 +228,8 @@ public class LeaiSyncService implements ILeaiSyncService { .lt(UgcWork::getLeaiStatus, remoteStatus) .set(UgcWork::getLeaiStatus, remoteStatus); - // 当 leaiStatus 推进到 CATALOGED 且当前 status 仍为 draft → 自动设 status 为 unpublished - if (remoteStatus >= LeaiCreationStatus.CATALOGED + // 当 leaiStatus 推进到 DUBBED 且当前 status 仍为 draft → 自动设 status 为 unpublished + if (remoteStatus >= LeaiCreationStatus.DUBBED && WorkPublishStatus.DRAFT.getValue().equals(work.getStatus())) { wrapper.set(UgcWork::getStatus, WorkPublishStatus.UNPUBLISHED.getValue()); } diff --git a/backend-java/src/main/java/com/competition/modules/pub/controller/PublicAuthController.java b/backend-java/src/main/java/com/competition/modules/pub/controller/PublicAuthController.java index 963fb92..9aeb045 100644 --- a/backend-java/src/main/java/com/competition/modules/pub/controller/PublicAuthController.java +++ b/backend-java/src/main/java/com/competition/modules/pub/controller/PublicAuthController.java @@ -5,6 +5,7 @@ import com.competition.common.result.Result; import com.competition.common.util.SecurityUtil; import com.competition.modules.pub.dto.PublicLoginDto; 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.service.PublicAuthService; import com.competition.modules.pub.service.SmsCodeService; @@ -42,6 +43,14 @@ public class PublicAuthController { return Result.success(publicAuthService.login(dto)); } + @Public + @PostMapping("/login/sms") + @RateLimit(permits = 5, duration = 1) + @Operation(summary = "手机号验证码登录") + public Result> loginBySms(@Valid @RequestBody PublicSmsLoginDto dto) { + return Result.success(publicAuthService.loginBySms(dto)); + } + @Public @PostMapping("/sms/send") @RateLimit(permits = 1, duration = 60) diff --git a/backend-java/src/main/java/com/competition/modules/pub/dto/PublicSmsLoginDto.java b/backend-java/src/main/java/com/competition/modules/pub/dto/PublicSmsLoginDto.java new file mode 100644 index 0000000..62933e6 --- /dev/null +++ b/backend-java/src/main/java/com/competition/modules/pub/dto/PublicSmsLoginDto.java @@ -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; +} diff --git a/backend-java/src/main/java/com/competition/modules/pub/service/PublicAuthService.java b/backend-java/src/main/java/com/competition/modules/pub/service/PublicAuthService.java index 67dc843..c86a3d7 100644 --- a/backend-java/src/main/java/com/competition/modules/pub/service/PublicAuthService.java +++ b/backend-java/src/main/java/com/competition/modules/pub/service/PublicAuthService.java @@ -9,6 +9,7 @@ import com.competition.common.enums.UserSource; import com.competition.common.enums.UserType; import com.competition.modules.pub.dto.PublicLoginDto; 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.SysTenant; import com.competition.modules.sys.entity.SysUser; @@ -160,6 +161,49 @@ public class PublicAuthService { return buildAuthResult(token, user, roles); } + /** + * 手机号验证码登录 + */ + public Map loginBySms(PublicSmsLoginDto dto) { + // 校验短信验证码 + smsCodeService.verifyCode(dto.getPhone(), dto.getSmsCode()); + + // 先在公众租户下按手机号查找用户 + SysTenant publicTenant = sysTenantMapper.selectOne( + new LambdaQueryWrapper().eq(SysTenant::getCode, TenantConstants.CODE_PUBLIC)); + + SysUser user = null; + if (publicTenant != null) { + user = sysUserMapper.selectOne( + new LambdaQueryWrapper() + .eq(SysUser::getPhone, dto.getPhone()) + .eq(SysUser::getTenantId, publicTenant.getId())); + } + + // 如未找到,在所有租户中搜索 + if (user == null) { + user = sysUserMapper.selectOne( + new LambdaQueryWrapper() + .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 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); + } + /** * 切换到子女账号 */ diff --git a/backend-java/src/main/java/com/competition/modules/ugc/entity/UgcWork.java b/backend-java/src/main/java/com/competition/modules/ugc/entity/UgcWork.java index c9b1f23..1ec2444 100644 --- a/backend-java/src/main/java/com/competition/modules/ugc/entity/UgcWork.java +++ b/backend-java/src/main/java/com/competition/modules/ugc/entity/UgcWork.java @@ -45,7 +45,7 @@ public class UgcWork implements Serializable { @Schema(description = "本地发布状态: draft/unpublished/pending_review/published/rejected") 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") private Integer leaiStatus; diff --git a/backend-java/src/main/resources/db/migration/V17__split_work_status.sql b/backend-java/src/main/resources/db/migration/V17__split_work_status.sql index 4a051c9..1f03ff2 100644 --- a/backend-java/src/main/resources/db/migration/V17__split_work_status.sql +++ b/backend-java/src/main/resources/db/migration/V17__split_work_status.sql @@ -4,7 +4,7 @@ -- 1. 新增 leai_status 字段 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 UPDATE t_ugc_work SET leai_status = status; diff --git a/backend-java/src/main/resources/db/migration/V5__leai_integration.sql b/backend-java/src/main/resources/db/migration/V5__leai_integration.sql index 64d7680..4ef6b1a 100644 --- a/backend-java/src/main/resources/db/migration/V5__leai_integration.sql +++ b/backend-java/src/main/resources/db/migration/V5__leai_integration.sql @@ -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]+$'; 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. 新增乐读派关联字段 ALTER TABLE t_ugc_work ADD COLUMN remote_work_id VARCHAR(64) DEFAULT NULL COMMENT '乐读派远程作品ID' AFTER user_id; diff --git a/docs/design/public/ugc-work-status-redesign.md b/docs/design/public/ugc-work-status-redesign.md index 1ae16b1..689bbfe 100644 --- a/docs/design/public/ugc-work-status-redesign.md +++ b/docs/design/public/ugc-work-status-redesign.md @@ -46,8 +46,8 @@ draft / pending_review / published / rejected 引入新的中间状态 **`unpublished`(未发布)**,明确语义边界: -- **草稿(draft)** = 作品技术上还没完成(生成失败 / 还在生成 / 没保存编目) -- **未发布(unpublished)** = 作品技术上完整(编目已保存),但用户没主动公开 +- **草稿(draft)** = 作品技术上还没完成(生成失败 / 还在生成 / 没完成配音) +- **未发布(unpublished)** = 作品技术上完整(配音已完成),但用户没主动公开 - **审核中(pending_review)** = 用户主动提交审核 - **已发布(published)** = 审核通过,发现页可见 - **被拒绝(rejected)** = 审核未通过,可改后重新提交 @@ -70,16 +70,16 @@ draft / pending_review / published / rejected │ ▼ ┌───────────────┐ - │ DRAFT │ 半成品:还在生成或未保存编目 + │ DRAFT │ 半成品:还在生成或未完成配音 │ 草稿 │ └───────┬───────┘ │ │ 用户在 EditInfoView 点 │ 保存 / 去配音 / 立即发布 - │ 任意按钮 = 编目完成 + │ 任意按钮 = 配音完成 ▼ ┌───────────────┐ - │ UNPUBLISHED │ 成品私有:已编目,可补配音,可发布 + │ UNPUBLISHED │ 成品私有:已配音,可发布 │ 未发布 │◀──────────────┐ └───────┬───────┘ │ │ │ @@ -111,7 +111,7 @@ draft / pending_review / published / rejected | 起始状态 | 触发 | 目标状态 | 触发方 | 接口 | |---|---|---|---|---| | (无) | 创建作品 | 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 | 用户补充配音/编辑 | UNPUBLISHED | 用户 | 更新内容接口 | | PENDING_REVIEW | 审核通过 | PUBLISHED | 超管 | `POST /content-review/works/{id}/approve` | @@ -218,7 +218,7 @@ taken_down → -2 -- 改为: ALTER TABLE t_ugc_work 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 跟前端对齐) 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 进度) - 改 `status: String`(本地发布状态,对应新枚举) - [ ] **新建 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` - [ ] **新增 `POST /public/works/{id}/unpublish`**:用户主动下架,`published → unpublished` - [ ] **新增 `POST /public/works/{id}/withdraw`**:用户撤回审核,`pending_review → unpublished` diff --git a/frontend/src/api/public.ts b/frontend/src/api/public.ts index 960edb2..f494033 100644 --- a/frontend/src/api/public.ts +++ b/frontend/src/api/public.ts @@ -48,7 +48,8 @@ publicApi.interceptors.response.use( // 检查业务状态码,非 200 视为业务错误 const resData = response.data 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 } return Promise.reject(error) } @@ -86,6 +87,11 @@ export interface PublicLoginParams { password: string } +export interface PublicSmsLoginParams { + phone: string + smsCode: string +} + export interface PublicUser { id: number username: string @@ -116,6 +122,10 @@ export const publicAuthApi = { login: (data: PublicLoginParams): Promise => publicApi.post("/public/auth/login", data), + /** 手机号验证码登录 */ + loginBySms: (data: PublicSmsLoginParams): Promise => + publicApi.post("/public/auth/login/sms", data), + /** 发送短信验证码 */ sendSmsCode: (phone: string): Promise => publicApi.post("/public/auth/sms/send", { phone }), @@ -395,8 +405,8 @@ export const publicInteractionApi = { * published → unpublished(下架) * rejected → pending_review(改后重交) * - * - draft 草稿:作品技术上还没完成(生成失败 / 还在生成 / 没保存编目) - * - unpublished 未发布:作品技术上完整(编目已保存),但用户没主动公开 + * - draft 草稿:作品技术上还没完成(生成失败 / 还在生成 / 没完成配音) + * - unpublished 未发布:作品技术上完整(配音已完成),但用户没主动公开 * - pending_review 审核中:用户主动提交审核 * - published 已发布:审核通过,发现页可见 * - rejected 被拒绝:审核未通过 diff --git a/frontend/src/views/public/Login.vue b/frontend/src/views/public/Login.vue index e16ecef..d17d052 100644 --- a/frontend/src/views/public/Login.vue +++ b/frontend/src/views/public/Login.vue @@ -9,12 +9,12 @@ - + - + - +
- + - + + +
@@ -124,10 +130,11 @@ const router = useRouter() const route = useRoute() const loading = ref(false) const isRegister = ref(false) +const loginMethod = ref<'password' | 'sms'>('password') const form = reactive({ - username: "demo", - password: "demo123456", + username: import.meta.env.DEV ? "demo" : "", + password: import.meta.env.DEV ? "demo123456" : "", confirmPassword: "", nickname: "", phone: import.meta.env.DEV ? "13800138000" : "", @@ -141,35 +148,15 @@ let smsTimer: ReturnType | null = null const isPhoneValid = computed(() => /^1[3-9]\d{9}$/.test(form.phone)) -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 - } -} +// 控制字段显示的 computed +const showNicknameField = computed(() => isRegister.value) +const showPhoneField = computed(() => isRegister.value || (!isRegister.value && loginMethod.value === 'sms')) +const showSmsCodeField = computed(() => isRegister.value || (!isRegister.value && loginMethod.value === 'sms')) +const showUsernameField = computed(() => !isRegister.value && loginMethod.value === 'password') +const showPasswordField = computed(() => !isRegister.value && loginMethod.value === 'password') -// 组件销毁时清理定时器 -onUnmounted(() => { - if (smsTimer) clearInterval(smsTimer) -}) - -const rules: Record = { +// 基础校验规则 +const baseRules: Record = { username: [ { required: true, message: "请输入用户名", trigger: "blur" }, { min: 4, message: "用户名至少4个字符", trigger: "blur" }, @@ -205,6 +192,71 @@ const rules: Record = { ], } +// 根据当前模式动态生成校验规则 +const currentRules = computed(() => { + const rules: Record = {} + 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 () => { loading.value = true try { @@ -218,6 +270,12 @@ const handleSubmit = async () => { smsCode: form.smsCode, }) message.success("注册成功") + } else if (loginMethod.value === 'sms') { + result = await publicAuthApi.loginBySms({ + phone: form.phone, + smsCode: form.smsCode, + }) + message.success("登录成功") } else { result = await publicAuthApi.login({ 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 { - margin-top: 20px; + margin-top: 12px; font-size: 13px; color: #9ca3af; diff --git a/frontend/src/views/public/create/views/BookReaderView.vue b/frontend/src/views/public/create/views/BookReaderView.vue index 681fdf8..8e54f30 100644 --- a/frontend/src/views/public/create/views/BookReaderView.vue +++ b/frontend/src/views/public/create/views/BookReaderView.vue @@ -1,3 +1,6 @@ +