feat: 编目与配音页 work-form 整包提交(详情快照+字段覆盖)
docs: 新增乐读派作品表单 PUT/GET 外部对接说明 Made-with: Cursor
This commit is contained in:
parent
9ed641c6e3
commit
b2ae6653d5
169
docs/api/public-leai-work-form.md
Normal file
169
docs/api/public-leai-work-form.md
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
# 乐读派作品表单(本库快照)API
|
||||||
|
|
||||||
|
面向外部系统与本平台前端「创作壳」对齐:**不经过乐读派 B2 的 GET/PUT 作品详情**,直接读写本库 `t_ugc_work` / `t_ugc_work_page` 中的编目与分页快照。
|
||||||
|
|
||||||
|
| 项 | 说明 |
|
||||||
|
|----|------|
|
||||||
|
| Base URL | 部署域名 + **context-path `/api`**(见 `application.yml` `server.servlet.context-path`) |
|
||||||
|
| 完整路径前缀 | `/api/public/leai-works` |
|
||||||
|
| 认证 | **需要登录**。请求头携带 `Authorization: Bearer <JWT>`;与 Web 端一致时可加 `X-Tenant-Code`、`X-Tenant-Id`(见前端 `request.ts`) |
|
||||||
|
| 安全说明 | 该路径**不在** Spring Security 匿名白名单中,未带有效 Token 将返回 **401** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 保存编目 / 分页快照
|
||||||
|
|
||||||
|
### 请求
|
||||||
|
|
||||||
|
| 项 | 值 |
|
||||||
|
|----|-----|
|
||||||
|
| Method | **PUT** |
|
||||||
|
| URL | `/api/public/leai-works/{remoteWorkId}/work-form` |
|
||||||
|
| Content-Type | `application/json` |
|
||||||
|
|
||||||
|
### 路径参数
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `remoteWorkId` | string | 是 | 乐读派侧作品 ID(可能为大整数,**建议按字符串传输**;路径中需 URL 编码) |
|
||||||
|
|
||||||
|
**示例**
|
||||||
|
|
||||||
|
```http
|
||||||
|
PUT /api/public/leai-works/2044362486899150848/work-form HTTP/1.1
|
||||||
|
Host: <your-host>
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": "绘本标题",
|
||||||
|
"author": "作者署名",
|
||||||
|
"subtitle": "副标题",
|
||||||
|
"intro": "简介",
|
||||||
|
"tags": ["冒险", "成长"],
|
||||||
|
"pageList": [
|
||||||
|
{
|
||||||
|
"pageNum": 1,
|
||||||
|
"imageUrl": "https://...",
|
||||||
|
"text": "本页文案",
|
||||||
|
"audioUrl": "https://..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求体(JSON 对象)
|
||||||
|
|
||||||
|
- **不能为空**:`{}` 会返回 **400**(`请求体不能为空`)。
|
||||||
|
- 服务端从 body 中读取两类数据:
|
||||||
|
1. **编目字段**(可选子集):仅处理以下键,其余顶层键若存在会被忽略(除 `pageList` 外):
|
||||||
|
`author`、`title`、`subtitle`、`intro`、`tags`
|
||||||
|
2. **分页**:`pageList` 为数组时,按乐读派 `pageList` 形态写入本库分页表。
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `title` | string | 标题 |
|
||||||
|
| `author` | string | 作者署名 |
|
||||||
|
| `subtitle` | string | 副标题 |
|
||||||
|
| `intro` | string | 简介 |
|
||||||
|
| `tags` | string[] | 标签列表 |
|
||||||
|
| `pageList` | object[] | 分页列表;元素字段见下表 |
|
||||||
|
|
||||||
|
`pageList` 元素:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `pageNum` | number | 页码(与后端 `page_no` 对应) |
|
||||||
|
| `imageUrl` | string | 插图 URL |
|
||||||
|
| `text` | string | 本页文案 |
|
||||||
|
| `audioUrl` | string | 配音 URL;可为空字符串表示未配 |
|
||||||
|
|
||||||
|
### 业务规则(摘要)
|
||||||
|
|
||||||
|
- 作品必须属于**当前登录用户**且 `remote_work_id` 匹配,否则 **404**(`作品不存在或无权操作`)。
|
||||||
|
- 若本次请求更新了编目,且作品为草稿、且乐读派进度已 ≥「生成完成」,可能将发布状态从草稿置为未发布(见 `PublicLeaiWorkFormService`)。
|
||||||
|
- 若 `pageList` 非空,且**每一页**的 `audioUrl` 均为非空白字符串,则将 `leai_status` 更新为 **已配音(5)**。
|
||||||
|
|
||||||
|
### 成功响应
|
||||||
|
|
||||||
|
HTTP **200**,Body 为统一包装:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "success",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
(`data` 可能为 `null`,以实际返回为准。)
|
||||||
|
|
||||||
|
### 错误响应
|
||||||
|
|
||||||
|
| HTTP | code(body) | 典型 message |
|
||||||
|
|------|----------------|--------------|
|
||||||
|
| 400 | 400 | `remoteWorkId 无效` / `请求体不能为空` |
|
||||||
|
| 401 | 401 | 未认证或 Token 无效 |
|
||||||
|
| 404 | 404 | `作品不存在或无权操作` |
|
||||||
|
|
||||||
|
错误体结构与项目统一 `Result` 一致(含 `message` 等字段)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 查询本库作品详情(建议与 PUT 配合:先 GET 再改后 PUT)
|
||||||
|
|
||||||
|
便于外部系统做「读-改-写」合并,避免只提交部分字段时覆盖丢失其它字段。
|
||||||
|
|
||||||
|
| 项 | 值 |
|
||||||
|
|----|-----|
|
||||||
|
| Method | **GET** |
|
||||||
|
| URL | `/api/public/leai-works/{remoteWorkId}/work-form` |
|
||||||
|
| 认证 | 同 PUT,需 **Bearer JWT** |
|
||||||
|
|
||||||
|
### 成功响应 `data` 结构(对象)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `workId` | string | 与路径中 `remoteWorkId` 一致(乐读派 ID) |
|
||||||
|
| `status` | number | 乐读派创作进度 `leai_status`,见下表 |
|
||||||
|
| `title` | string | 标题 |
|
||||||
|
| `author` | string | 作者 |
|
||||||
|
| `coverUrl` | string | 封面 URL |
|
||||||
|
| `subtitle` | string | 副标题 |
|
||||||
|
| `intro` | string | 简介 |
|
||||||
|
| `tags` | array | 标签 |
|
||||||
|
| `pageList` | object[] | 分页;元素含 `pageNum`、`imageUrl`、`text`、`audioUrl` |
|
||||||
|
|
||||||
|
### `status`(`leai_status`)取值参考
|
||||||
|
|
||||||
|
| 值 | 含义 |
|
||||||
|
|----|------|
|
||||||
|
| -1 | 失败 |
|
||||||
|
| 0 | 草稿 |
|
||||||
|
| 1 | 待处理 |
|
||||||
|
| 2 | 处理中 |
|
||||||
|
| 3 | 生成完成 |
|
||||||
|
| 4 | 已编目 |
|
||||||
|
| 5 | 已配音 |
|
||||||
|
|
||||||
|
(定义见后端 `LeaiCreationStatus`。)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 对接建议
|
||||||
|
|
||||||
|
1. **大整数 ID**:`remoteWorkId` 在部分语言中超出 IEEE754 安全整数,**全程使用字符串**。
|
||||||
|
2. **合并提交**:若只改编目,可先 **GET** 全量,在内存中覆盖 `author`/`subtitle`/`intro`/`tags`/`title` 等字段后再 **PUT** 全量,与当前前端「快照 + 覆盖」策略一致,避免清空 `pageList`。
|
||||||
|
3. **仅改分页**:可只带 `pageList`(及必须的非空 body);编目键可选。
|
||||||
|
4. **Swagger**:若服务开启 Knife4j,可在 `/doc.html` 查看 **公众端-乐读派作品表单** 分组(以部署为准)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 实现位置(源码索引)
|
||||||
|
|
||||||
|
| 说明 | 路径 |
|
||||||
|
|------|------|
|
||||||
|
| Controller | `lesingle-creation-backend/.../pub/controller/PublicLeaiWorkController.java` |
|
||||||
|
| 业务 | `lesingle-creation-backend/.../pub/service/PublicLeaiWorkFormService.java` |
|
||||||
|
|
||||||
|
文档版本与接口行为以服务端代码为准;若行为变更请同步更新本文档。
|
||||||
@ -146,18 +146,28 @@ import {
|
|||||||
ArrowRightOutlined,
|
ArrowRightOutlined,
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import PageHeader from '@/components/aicreate/PageHeader.vue'
|
import PageHeader from '@/components/aicreate/PageHeader.vue'
|
||||||
import { voicePage, ossUpload, batchUpdateAudio, finishDubbing } from '@/api/aicreate'
|
import { voicePage, ossUpload } from '@/api/aicreate'
|
||||||
import { getLeaiWorkFormDetail, saveLeaiWorkForm } from '@/api/public'
|
import { getLeaiWorkFormDetail, saveLeaiWorkForm } from '@/api/public'
|
||||||
import { useAicreateStore } from '@/stores/aicreate'
|
|
||||||
import { STATUS, getRouteByStatus } from '@/utils/aicreate/status'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const store = useAicreateStore()
|
const workId = computed(() => route.params.workId)
|
||||||
const workId = computed(() => route.params.workId || store.workId)
|
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
/** getLeaiWorkFormDetail 完整快照;提交时在快照上覆盖当前页的 pageList(及配音结果),整包提交避免丢失编目等字段 */
|
||||||
|
const workFormSnapshot = ref<Record<string, unknown> | null>(null)
|
||||||
|
|
||||||
|
function cloneWorkFormSnapshot(raw: Record<string, unknown> | null) {
|
||||||
|
if (raw == null) return null
|
||||||
|
try {
|
||||||
|
return JSON.parse(JSON.stringify(raw)) as Record<string, unknown>
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const pages = ref<any[]>([])
|
const pages = ref<any[]>([])
|
||||||
const idx = ref(0)
|
const idx = ref(0)
|
||||||
const toast = ref('')
|
const toast = ref('')
|
||||||
@ -430,23 +440,22 @@ async function voiceAllConfirm() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 乐读派分页形态 + 可选编目字段,写入本库 t_ugc_work / t_ugc_work_page */
|
/** 详情快照 + 当前分页/配音,整包 PUT,避免只传 pageList 时清空编目等字段 */
|
||||||
function buildWorkFormPayload() {
|
function buildWorkFormPayload() {
|
||||||
const pageList = pages.value.map(p => ({
|
const base = workFormSnapshot.value
|
||||||
|
if (!base) {
|
||||||
|
throw new Error('作品详情未加载')
|
||||||
|
}
|
||||||
|
const body = cloneWorkFormSnapshot(base)
|
||||||
|
if (!body) {
|
||||||
|
throw new Error('数据异常')
|
||||||
|
}
|
||||||
|
body.pageList = pages.value.map(p => ({
|
||||||
pageNum: p.pageNum,
|
pageNum: p.pageNum,
|
||||||
imageUrl: p.imageUrl,
|
imageUrl: p.imageUrl,
|
||||||
text: p.text,
|
text: p.text,
|
||||||
audioUrl: p.audioUrl || undefined,
|
audioUrl: p.audioUrl || undefined,
|
||||||
}))
|
}))
|
||||||
const wd = store.workDetail
|
|
||||||
const body = { pageList } as Record<string, unknown>
|
|
||||||
if (wd) {
|
|
||||||
if (wd.author) body.author = wd.author
|
|
||||||
if (wd.subtitle != null && wd.subtitle !== '') body.subtitle = wd.subtitle
|
|
||||||
if (wd.intro != null && wd.intro !== '') body.intro = wd.intro
|
|
||||||
if (Array.isArray(wd.tags) && wd.tags.length) body.tags = [...wd.tags]
|
|
||||||
if (wd.title) body.title = wd.title
|
|
||||||
}
|
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -457,25 +466,23 @@ async function finish() {
|
|||||||
const pendingLocal = pages.value.filter(p => p.localBlob)
|
const pendingLocal = pages.value.filter(p => p.localBlob)
|
||||||
|
|
||||||
if (pendingLocal.length > 0) {
|
if (pendingLocal.length > 0) {
|
||||||
const audioPages = []
|
|
||||||
for (let i = 0; i < pendingLocal.length; i++) {
|
for (let i = 0; i < pendingLocal.length; i++) {
|
||||||
const p = pendingLocal[i]
|
const p = pendingLocal[i]
|
||||||
showToast(`上传录音 ${i + 1}/${pendingLocal.length}…`)
|
showToast(`上传录音 ${i + 1}/${pendingLocal.length}…`)
|
||||||
const ext = p.localBlob.type?.includes('webm') ? 'webm' : 'm4a'
|
const ext = p.localBlob.type?.includes('webm') ? 'webm' : 'm4a'
|
||||||
const ossUrl = await ossUpload(p.localBlob, { type: 'aud', ext })
|
const ossUrl = await ossUpload(p.localBlob, { type: 'aud', ext })
|
||||||
audioPages.push({ pageNum: p.pageNum, audioUrl: ossUrl })
|
|
||||||
p.audioUrl = ossUrl
|
p.audioUrl = ossUrl
|
||||||
p.localBlob = null
|
p.localBlob = null
|
||||||
}
|
}
|
||||||
showToast('保存配音…')
|
showToast('保存配音…')
|
||||||
await batchUpdateAudio(workId.value, audioPages)
|
|
||||||
} else {
|
} else {
|
||||||
showToast('完成配音…')
|
showToast('完成配音…')
|
||||||
await finishDubbing(workId.value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await saveLeaiWorkForm(String(workId.value || ''), buildWorkFormPayload())
|
// 与编目页一致:仅 PUT /public/leai-works/{remoteWorkId}/work-form 写入本库分页与配音,不走 POST /leai-proxy/batch-audio
|
||||||
store.workDetail = null
|
const payload = buildWorkFormPayload()
|
||||||
|
await saveLeaiWorkForm(String(workId.value || ''), payload)
|
||||||
|
workFormSnapshot.value = cloneWorkFormSnapshot(payload)
|
||||||
showToast('配音完成')
|
showToast('配音完成')
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() =>
|
() =>
|
||||||
@ -489,12 +496,14 @@ async function finish() {
|
|||||||
try {
|
try {
|
||||||
const check = await getLeaiWorkFormDetail(String(workId.value || ''))
|
const check = await getLeaiWorkFormDetail(String(workId.value || ''))
|
||||||
if (check?.status >= 5) {
|
if (check?.status >= 5) {
|
||||||
|
workFormSnapshot.value = cloneWorkFormSnapshot(check as Record<string, unknown>)
|
||||||
try {
|
try {
|
||||||
await saveLeaiWorkForm(String(workId.value || ''), buildWorkFormPayload())
|
const retryPayload = buildWorkFormPayload()
|
||||||
|
await saveLeaiWorkForm(String(workId.value || ''), retryPayload)
|
||||||
|
workFormSnapshot.value = cloneWorkFormSnapshot(retryPayload)
|
||||||
} catch {
|
} catch {
|
||||||
/* 本库无记录时忽略 */
|
/* 本库无记录时忽略 */
|
||||||
}
|
}
|
||||||
store.workDetail = null
|
|
||||||
showToast('配音已完成')
|
showToast('配音已完成')
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() =>
|
() =>
|
||||||
@ -515,23 +524,11 @@ async function finish() {
|
|||||||
|
|
||||||
// --- Load ---
|
// --- Load ---
|
||||||
async function loadWork() {
|
async function loadWork() {
|
||||||
store.reset();
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
if (!store.workDetail || store.workDetail.workId !== workId.value) {
|
const res = await getLeaiWorkFormDetail(String(workId.value || ''))
|
||||||
store.workDetail = null
|
workFormSnapshot.value = cloneWorkFormSnapshot(res as Record<string, unknown>)
|
||||||
const res = await getLeaiWorkFormDetail(String(workId.value || ''))
|
const w = res as any
|
||||||
store.workDetail = res
|
|
||||||
}
|
|
||||||
const w = store.workDetail
|
|
||||||
|
|
||||||
// 仅当尚未编目完成(CATALOGED)时按流程跳转(如 COMPLETED→预览);已 DUBBED 仍允许停留本页,
|
|
||||||
// 否则从编辑页点「去配音」会立刻被 getRouteByStatus(DUBBED→EditInfo) 打回当前页。
|
|
||||||
if (w.status < STATUS.CATALOGED) {
|
|
||||||
const wid = String(w.workId ?? workId.value ?? '')
|
|
||||||
const nextRoute = getRouteByStatus(w.status, wid)
|
|
||||||
if (nextRoute) { router.replace(nextRoute); return }
|
|
||||||
}
|
|
||||||
|
|
||||||
pages.value = (w.pageList || []).map((p: any) => ({
|
pages.value = (w.pageList || []).map((p: any) => ({
|
||||||
pageNum: p.pageNum,
|
pageNum: p.pageNum,
|
||||||
@ -541,7 +538,9 @@ async function loadWork() {
|
|||||||
localBlob: null,
|
localBlob: null,
|
||||||
isAiVoice: p.audioUrl ? true : null,
|
isAiVoice: p.audioUrl ? true : null,
|
||||||
}))
|
}))
|
||||||
} catch { /* fallback */ }
|
} catch {
|
||||||
|
workFormSnapshot.value = null
|
||||||
|
}
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export default { name: 'EditInfoView' }
|
|||||||
<!-- 封面预览 -->
|
<!-- 封面预览 -->
|
||||||
<div class="cover-preview" v-if="coverUrl">
|
<div class="cover-preview" v-if="coverUrl">
|
||||||
<img :src="coverUrl" class="cover-img" />
|
<img :src="coverUrl" class="cover-img" />
|
||||||
<div class="cover-title-overlay">{{ store.workDetail?.title || '未命名' }}</div>
|
<div class="cover-title-overlay">{{ catalogMeta.title || '未命名' }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 基本信息 -->
|
<!-- 基本信息 -->
|
||||||
@ -132,14 +132,10 @@ import {
|
|||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import PageHeader from '@/components/aicreate/PageHeader.vue'
|
import PageHeader from '@/components/aicreate/PageHeader.vue'
|
||||||
import { getLeaiWorkFormDetail, publicUserWorksApi, saveLeaiWorkForm } from '@/api/public'
|
import { getLeaiWorkFormDetail, publicUserWorksApi, saveLeaiWorkForm } from '@/api/public'
|
||||||
import { useAicreateStore } from '@/stores/aicreate'
|
|
||||||
import { clearExtractDraft } from '@/utils/aicreate/extractDraft'
|
|
||||||
import { STATUS, getRouteByStatus } from '@/utils/aicreate/status'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const store = useAicreateStore()
|
const workId = computed(() => route.params.workId)
|
||||||
const workId = computed(() => route.params.workId || store.workId)
|
|
||||||
|
|
||||||
/** 乐读派 workId 可能超过 JS 安全整数,请求与比较一律用路由字符串 */
|
/** 乐读派 workId 可能超过 JS 安全整数,请求与比较一律用路由字符串 */
|
||||||
function resolvedWorkIdStr() {
|
function resolvedWorkIdStr() {
|
||||||
@ -152,12 +148,28 @@ const loading = ref(true)
|
|||||||
const processing = ref(false)
|
const processing = ref(false)
|
||||||
const coverUrl = ref('')
|
const coverUrl = ref('')
|
||||||
|
|
||||||
|
/** 只读:详情接口中的非表单项(封面标题、流程状态),不参与 v-model */
|
||||||
|
const catalogMeta = ref({ title: '', status: 0 })
|
||||||
|
|
||||||
|
/** 仅编辑:作者 / 副标题 / 简介 / 标签(与 saveLeaiWorkForm 编目字段一致) */
|
||||||
const form = ref({ author: '', subtitle: '', intro: '' })
|
const form = ref({ author: '', subtitle: '', intro: '' })
|
||||||
const selectedTags = ref([])
|
const selectedTags = ref([])
|
||||||
const addingTag = ref(false)
|
const addingTag = ref(false)
|
||||||
const newTag = ref('')
|
const newTag = ref('')
|
||||||
const tagInput = ref(null)
|
const tagInput = ref(null)
|
||||||
|
|
||||||
|
/** getLeaiWorkFormDetail 返回的完整快照(深拷贝),保存时用表单同名字段覆盖后再整包提交,避免丢失 pageList 等未编辑字段 */
|
||||||
|
const workFormSnapshot = ref(null)
|
||||||
|
|
||||||
|
function cloneWorkFormSnapshot(raw) {
|
||||||
|
if (raw == null) return null
|
||||||
|
try {
|
||||||
|
return JSON.parse(JSON.stringify(raw))
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const PRESET_TAGS = ['冒险', '成长', '友谊', '魔法', '勇敢', '快乐', '温暖', '探索', '梦想', '好奇']
|
const PRESET_TAGS = ['冒险', '成长', '友谊', '魔法', '勇敢', '快乐', '温暖', '探索', '梦想', '好奇']
|
||||||
const availablePresets = computed(() =>
|
const availablePresets = computed(() =>
|
||||||
PRESET_TAGS.filter(t => !selectedTags.value.includes(t))
|
PRESET_TAGS.filter(t => !selectedTags.value.includes(t))
|
||||||
@ -187,6 +199,21 @@ function confirmAddTag() {
|
|||||||
addingTag.value = false
|
addingTag.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 将详情接口数据灌入表单与只读展示(本页不持有整份 workDetail,仅映射所需字段) */
|
||||||
|
function applyDetailToForm(w) {
|
||||||
|
catalogMeta.value = {
|
||||||
|
title: w.title || '',
|
||||||
|
status: Number(w.status) || 0,
|
||||||
|
}
|
||||||
|
form.value = {
|
||||||
|
author: w.author || '',
|
||||||
|
subtitle: w.subtitle || '',
|
||||||
|
intro: w.intro || '',
|
||||||
|
}
|
||||||
|
selectedTags.value = Array.isArray(w.tags) && w.tags.length ? [...w.tags] : ['冒险']
|
||||||
|
coverUrl.value = w.coverUrl || w.pageList?.[0]?.imageUrl || ''
|
||||||
|
}
|
||||||
|
|
||||||
async function loadWork() {
|
async function loadWork() {
|
||||||
const id = resolvedWorkIdStr()
|
const id = resolvedWorkIdStr()
|
||||||
if (!id) {
|
if (!id) {
|
||||||
@ -195,24 +222,11 @@ async function loadWork() {
|
|||||||
}
|
}
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
// 必须每次拉取详情:创作壳 keep-alive 会缓存组件,仅靠 store 命中会跳过请求,二次进入表单仍是旧值
|
|
||||||
store.workDetail = null
|
|
||||||
const res = await getLeaiWorkFormDetail(id)
|
const res = await getLeaiWorkFormDetail(id)
|
||||||
store.workDetail = res
|
workFormSnapshot.value = cloneWorkFormSnapshot(res)
|
||||||
const w = store.workDetail
|
applyDetailToForm(res)
|
||||||
|
|
||||||
// 已配音(DUBBED)仍可在本页编辑元数据/发布;仅当状态高于当前流程终态时再按 status 跳转
|
|
||||||
if (w.status > STATUS.DUBBED) {
|
|
||||||
const nextRoute = getRouteByStatus(w.status, id)
|
|
||||||
if (nextRoute) { router.replace(nextRoute); return }
|
|
||||||
}
|
|
||||||
|
|
||||||
form.value.author = w.author || ''
|
|
||||||
form.value.subtitle = w.subtitle || ''
|
|
||||||
form.value.intro = w.intro || ''
|
|
||||||
selectedTags.value = Array.isArray(w.tags) && w.tags.length ? [...w.tags] : ['冒险']
|
|
||||||
coverUrl.value = w.coverUrl || w.pageList?.[0]?.imageUrl || ''
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
workFormSnapshot.value = null
|
||||||
// fallback: proceed with empty form
|
// fallback: proceed with empty form
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
@ -241,19 +255,28 @@ async function saveFormToServer() {
|
|||||||
message.error('作品 ID 无效')
|
message.error('作品 ID 无效')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const data = { tags: selectedTags.value }
|
const base = workFormSnapshot.value
|
||||||
data.author = form.value.author.trim()
|
if (!base || typeof base !== 'object') {
|
||||||
data.subtitle = form.value.subtitle.trim()
|
message.error('作品详情未加载,请刷新后重试')
|
||||||
data.intro = form.value.intro.trim()
|
return false
|
||||||
|
|
||||||
await saveLeaiWorkForm(id, data)
|
|
||||||
|
|
||||||
if (store.workDetail) {
|
|
||||||
store.workDetail.author = data.author
|
|
||||||
store.workDetail.subtitle = data.subtitle
|
|
||||||
store.workDetail.intro = data.intro
|
|
||||||
store.workDetail.tags = [...selectedTags.value]
|
|
||||||
}
|
}
|
||||||
|
const payload = cloneWorkFormSnapshot(base)
|
||||||
|
if (!payload) {
|
||||||
|
message.error('数据异常,请刷新后重试')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
payload.author = form.value.author.trim()
|
||||||
|
payload.subtitle = form.value.subtitle.trim()
|
||||||
|
payload.intro = form.value.intro.trim()
|
||||||
|
payload.tags = [...selectedTags.value]
|
||||||
|
|
||||||
|
await saveLeaiWorkForm(id, payload)
|
||||||
|
|
||||||
|
workFormSnapshot.value = cloneWorkFormSnapshot(payload)
|
||||||
|
form.value.author = payload.author
|
||||||
|
form.value.subtitle = payload.subtitle
|
||||||
|
form.value.intro = payload.intro
|
||||||
|
selectedTags.value = Array.isArray(payload.tags) ? [...payload.tags] : [...selectedTags.value]
|
||||||
return true
|
return true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
message.error(e.message || '保存失败,请重试')
|
message.error(e.message || '保存失败,请重试')
|
||||||
@ -267,7 +290,6 @@ async function handleSave() {
|
|||||||
processing.value = true
|
processing.value = true
|
||||||
try {
|
try {
|
||||||
if (await saveFormToServer()) {
|
if (await saveFormToServer()) {
|
||||||
store.workDetail = null
|
|
||||||
router.push({
|
router.push({
|
||||||
name: 'PublicCreateSaveSuccess',
|
name: 'PublicCreateSaveSuccess',
|
||||||
params: { workId: resolvedWorkIdStr() },
|
params: { workId: resolvedWorkIdStr() },
|
||||||
@ -284,7 +306,6 @@ async function handleGoDubbing() {
|
|||||||
processing.value = true
|
processing.value = true
|
||||||
try {
|
try {
|
||||||
if (await saveFormToServer()) {
|
if (await saveFormToServer()) {
|
||||||
store.workDetail = null
|
|
||||||
router.push(`/p/create/dubbing/${resolvedWorkIdStr()}`)
|
router.push(`/p/create/dubbing/${resolvedWorkIdStr()}`)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@ -336,7 +357,6 @@ async function handlePublish() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
store.workDetail = null
|
|
||||||
message.success('提交成功,作品已进入审核,可在作品库「审核中」查看进度')
|
message.success('提交成功,作品已进入审核,可在作品库「审核中」查看进度')
|
||||||
router.push({
|
router.push({
|
||||||
name: 'PublicCreateSaveSuccess',
|
name: 'PublicCreateSaveSuccess',
|
||||||
@ -360,8 +380,6 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
store.reset();
|
|
||||||
clearExtractDraft()
|
|
||||||
loadWork()
|
loadWork()
|
||||||
nextTick(() => { if (tagInput.value) tagInput.value.focus() })
|
nextTick(() => { if (tagInput.value) tagInput.value.focus() })
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user