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,
|
||||
} from '@ant-design/icons-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 { useAicreateStore } from '@/stores/aicreate'
|
||||
import { STATUS, getRouteByStatus } from '@/utils/aicreate/status'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const store = useAicreateStore()
|
||||
const workId = computed(() => route.params.workId || store.workId)
|
||||
const workId = computed(() => route.params.workId)
|
||||
|
||||
const loading = ref(true)
|
||||
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 idx = ref(0)
|
||||
const toast = ref('')
|
||||
@ -430,23 +440,22 @@ async function voiceAllConfirm() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 乐读派分页形态 + 可选编目字段,写入本库 t_ugc_work / t_ugc_work_page */
|
||||
/** 详情快照 + 当前分页/配音,整包 PUT,避免只传 pageList 时清空编目等字段 */
|
||||
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,
|
||||
imageUrl: p.imageUrl,
|
||||
text: p.text,
|
||||
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
|
||||
}
|
||||
|
||||
@ -457,25 +466,23 @@ async function finish() {
|
||||
const pendingLocal = pages.value.filter(p => p.localBlob)
|
||||
|
||||
if (pendingLocal.length > 0) {
|
||||
const audioPages = []
|
||||
for (let i = 0; i < pendingLocal.length; i++) {
|
||||
const p = pendingLocal[i]
|
||||
showToast(`上传录音 ${i + 1}/${pendingLocal.length}…`)
|
||||
const ext = p.localBlob.type?.includes('webm') ? 'webm' : 'm4a'
|
||||
const ossUrl = await ossUpload(p.localBlob, { type: 'aud', ext })
|
||||
audioPages.push({ pageNum: p.pageNum, audioUrl: ossUrl })
|
||||
p.audioUrl = ossUrl
|
||||
p.localBlob = null
|
||||
}
|
||||
showToast('保存配音…')
|
||||
await batchUpdateAudio(workId.value, audioPages)
|
||||
} else {
|
||||
showToast('完成配音…')
|
||||
await finishDubbing(workId.value)
|
||||
}
|
||||
|
||||
await saveLeaiWorkForm(String(workId.value || ''), buildWorkFormPayload())
|
||||
store.workDetail = null
|
||||
// 与编目页一致:仅 PUT /public/leai-works/{remoteWorkId}/work-form 写入本库分页与配音,不走 POST /leai-proxy/batch-audio
|
||||
const payload = buildWorkFormPayload()
|
||||
await saveLeaiWorkForm(String(workId.value || ''), payload)
|
||||
workFormSnapshot.value = cloneWorkFormSnapshot(payload)
|
||||
showToast('配音完成')
|
||||
setTimeout(
|
||||
() =>
|
||||
@ -489,12 +496,14 @@ async function finish() {
|
||||
try {
|
||||
const check = await getLeaiWorkFormDetail(String(workId.value || ''))
|
||||
if (check?.status >= 5) {
|
||||
workFormSnapshot.value = cloneWorkFormSnapshot(check as Record<string, unknown>)
|
||||
try {
|
||||
await saveLeaiWorkForm(String(workId.value || ''), buildWorkFormPayload())
|
||||
const retryPayload = buildWorkFormPayload()
|
||||
await saveLeaiWorkForm(String(workId.value || ''), retryPayload)
|
||||
workFormSnapshot.value = cloneWorkFormSnapshot(retryPayload)
|
||||
} catch {
|
||||
/* 本库无记录时忽略 */
|
||||
}
|
||||
store.workDetail = null
|
||||
showToast('配音已完成')
|
||||
setTimeout(
|
||||
() =>
|
||||
@ -515,23 +524,11 @@ async function finish() {
|
||||
|
||||
// --- Load ---
|
||||
async function loadWork() {
|
||||
store.reset();
|
||||
loading.value = true
|
||||
try {
|
||||
if (!store.workDetail || store.workDetail.workId !== workId.value) {
|
||||
store.workDetail = null
|
||||
const res = await getLeaiWorkFormDetail(String(workId.value || ''))
|
||||
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 }
|
||||
}
|
||||
const res = await getLeaiWorkFormDetail(String(workId.value || ''))
|
||||
workFormSnapshot.value = cloneWorkFormSnapshot(res as Record<string, unknown>)
|
||||
const w = res as any
|
||||
|
||||
pages.value = (w.pageList || []).map((p: any) => ({
|
||||
pageNum: p.pageNum,
|
||||
@ -541,7 +538,9 @@ async function loadWork() {
|
||||
localBlob: null,
|
||||
isAiVoice: p.audioUrl ? true : null,
|
||||
}))
|
||||
} catch { /* fallback */ }
|
||||
} catch {
|
||||
workFormSnapshot.value = null
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ export default { name: 'EditInfoView' }
|
||||
<!-- 封面预览 -->
|
||||
<div class="cover-preview" v-if="coverUrl">
|
||||
<img :src="coverUrl" class="cover-img" />
|
||||
<div class="cover-title-overlay">{{ store.workDetail?.title || '未命名' }}</div>
|
||||
<div class="cover-title-overlay">{{ catalogMeta.title || '未命名' }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
@ -132,14 +132,10 @@ import {
|
||||
import { message } from 'ant-design-vue'
|
||||
import PageHeader from '@/components/aicreate/PageHeader.vue'
|
||||
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 route = useRoute()
|
||||
const store = useAicreateStore()
|
||||
const workId = computed(() => route.params.workId || store.workId)
|
||||
const workId = computed(() => route.params.workId)
|
||||
|
||||
/** 乐读派 workId 可能超过 JS 安全整数,请求与比较一律用路由字符串 */
|
||||
function resolvedWorkIdStr() {
|
||||
@ -152,12 +148,28 @@ const loading = ref(true)
|
||||
const processing = ref(false)
|
||||
const coverUrl = ref('')
|
||||
|
||||
/** 只读:详情接口中的非表单项(封面标题、流程状态),不参与 v-model */
|
||||
const catalogMeta = ref({ title: '', status: 0 })
|
||||
|
||||
/** 仅编辑:作者 / 副标题 / 简介 / 标签(与 saveLeaiWorkForm 编目字段一致) */
|
||||
const form = ref({ author: '', subtitle: '', intro: '' })
|
||||
const selectedTags = ref([])
|
||||
const addingTag = ref(false)
|
||||
const newTag = ref('')
|
||||
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 availablePresets = computed(() =>
|
||||
PRESET_TAGS.filter(t => !selectedTags.value.includes(t))
|
||||
@ -187,6 +199,21 @@ function confirmAddTag() {
|
||||
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() {
|
||||
const id = resolvedWorkIdStr()
|
||||
if (!id) {
|
||||
@ -195,24 +222,11 @@ async function loadWork() {
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
// 必须每次拉取详情:创作壳 keep-alive 会缓存组件,仅靠 store 命中会跳过请求,二次进入表单仍是旧值
|
||||
store.workDetail = null
|
||||
const res = await getLeaiWorkFormDetail(id)
|
||||
store.workDetail = res
|
||||
const w = store.workDetail
|
||||
|
||||
// 已配音(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 || ''
|
||||
workFormSnapshot.value = cloneWorkFormSnapshot(res)
|
||||
applyDetailToForm(res)
|
||||
} catch (e) {
|
||||
workFormSnapshot.value = null
|
||||
// fallback: proceed with empty form
|
||||
} finally {
|
||||
loading.value = false
|
||||
@ -241,19 +255,28 @@ async function saveFormToServer() {
|
||||
message.error('作品 ID 无效')
|
||||
return false
|
||||
}
|
||||
const data = { tags: selectedTags.value }
|
||||
data.author = form.value.author.trim()
|
||||
data.subtitle = form.value.subtitle.trim()
|
||||
data.intro = form.value.intro.trim()
|
||||
|
||||
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 base = workFormSnapshot.value
|
||||
if (!base || typeof base !== 'object') {
|
||||
message.error('作品详情未加载,请刷新后重试')
|
||||
return false
|
||||
}
|
||||
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
|
||||
} catch (e) {
|
||||
message.error(e.message || '保存失败,请重试')
|
||||
@ -267,7 +290,6 @@ async function handleSave() {
|
||||
processing.value = true
|
||||
try {
|
||||
if (await saveFormToServer()) {
|
||||
store.workDetail = null
|
||||
router.push({
|
||||
name: 'PublicCreateSaveSuccess',
|
||||
params: { workId: resolvedWorkIdStr() },
|
||||
@ -284,7 +306,6 @@ async function handleGoDubbing() {
|
||||
processing.value = true
|
||||
try {
|
||||
if (await saveFormToServer()) {
|
||||
store.workDetail = null
|
||||
router.push(`/p/create/dubbing/${resolvedWorkIdStr()}`)
|
||||
}
|
||||
} finally {
|
||||
@ -336,7 +357,6 @@ async function handlePublish() {
|
||||
}
|
||||
}
|
||||
|
||||
store.workDetail = null
|
||||
message.success('提交成功,作品已进入审核,可在作品库「审核中」查看进度')
|
||||
router.push({
|
||||
name: 'PublicCreateSaveSuccess',
|
||||
@ -360,8 +380,6 @@ watch(
|
||||
)
|
||||
|
||||
onActivated(() => {
|
||||
store.reset();
|
||||
clearExtractDraft()
|
||||
loadWork()
|
||||
nextTick(() => { if (tagInput.value) tagInput.value.focus() })
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user