fix: 乐读派作品元数据本地同步与创作流程路由修复
- 后端:PUT 代理成功码兼容 0/200;syncWork 在状态未变时同步元数据;请求体覆盖本地 t_ugc_work - 前端:EditInfo 强制拉详情、workId 字符串化、副标题/简介完整提交 - 评委管理:联系方式增加手机号校验 - Dubbing:仅未编目完成时按状态跳转,避免已配音作品从编辑页进配音被立即打回 Made-with: Cursor
This commit is contained in:
parent
b11cb4b9d7
commit
eff55b6f7b
@ -160,40 +160,48 @@ public class LeaiProxyController {
|
|||||||
body.putAll(requestBody);
|
body.putAll(requestBody);
|
||||||
log.info("[乐读派代理] 编辑作品, workId={}", id);
|
log.info("[乐读派代理] 编辑作品, workId={}", id);
|
||||||
String responseBody = leaiApiClient.proxyPut("/update/work/" + id, body);
|
String responseBody = leaiApiClient.proxyPut("/update/work/" + id, body);
|
||||||
syncLocalAfterLeaiUpdate(id, responseBody);
|
syncLocalAfterLeaiUpdate(id, responseBody, requestBody);
|
||||||
return jsonOk(responseBody);
|
return jsonOk(responseBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析乐读派 update 响应并同步本地 t_ugc_work,避免仅依赖 Webhook 导致发布前状态滞后
|
* 解析乐读派 update 响应并同步本地 t_ugc_work,避免仅依赖 Webhook 导致发布前状态滞后;
|
||||||
|
* 成功后额外用本次请求体覆盖本地元数据(与乐读派查询结果解耦,避免远端滞后)。
|
||||||
*/
|
*/
|
||||||
private void syncLocalAfterLeaiUpdate(String remoteWorkId, String responseJson) {
|
private void syncLocalAfterLeaiUpdate(String remoteWorkId, String responseJson, Map<String, Object> proxyRequestBody) {
|
||||||
if (responseJson == null || responseJson.isEmpty()) {
|
if (responseJson != null && !responseJson.isEmpty()) {
|
||||||
return;
|
try {
|
||||||
|
Map<String, Object> root = objectMapper.readValue(responseJson, new TypeReference<Map<String, Object>>() {});
|
||||||
|
boolean ok = true;
|
||||||
|
if (root.containsKey("code")) {
|
||||||
|
int code = LeaiUtil.toInt(root.get("code"), 200);
|
||||||
|
// 乐读派部分接口成功为 0,与前端 publicApi 拦截器一致
|
||||||
|
if (code != 200 && code != 0) {
|
||||||
|
ok = false;
|
||||||
|
log.debug("[乐读派代理] update 响应非成功,跳过远端拉取同步: code={}", code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ok) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> data = (Map<String, Object>) root.get("data");
|
||||||
|
if (data == null) {
|
||||||
|
data = root;
|
||||||
|
}
|
||||||
|
if (data.get("status") == null) {
|
||||||
|
data = leaiApiClient.fetchWorkDetail(remoteWorkId);
|
||||||
|
}
|
||||||
|
if (data != null) {
|
||||||
|
leaiSyncService.syncWork(remoteWorkId, data, "Proxy[update.work]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[乐读派代理] 编辑作品后同步本地失败: remoteWorkId={}", remoteWorkId, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Map<String, Object> root = objectMapper.readValue(responseJson, new TypeReference<Map<String, Object>>() {});
|
leaiSyncService.applyLocalMetadataFromProxyPut(remoteWorkId, proxyRequestBody);
|
||||||
if (root.containsKey("code")) {
|
|
||||||
int code = LeaiUtil.toInt(root.get("code"), 200);
|
|
||||||
if (code != 200) {
|
|
||||||
log.debug("[乐读派代理] update 响应非成功,跳过本地同步: code={}", code);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Map<String, Object> data = (Map<String, Object>) root.get("data");
|
|
||||||
if (data == null) {
|
|
||||||
data = root;
|
|
||||||
}
|
|
||||||
if (data.get("status") == null) {
|
|
||||||
data = leaiApiClient.fetchWorkDetail(remoteWorkId);
|
|
||||||
if (data == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
leaiSyncService.syncWork(remoteWorkId, data, "Proxy[update.work]");
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("[乐读派代理] 编辑作品后同步本地失败: remoteWorkId={}", remoteWorkId, e);
|
log.warn("[乐读派代理] 本地元数据覆盖失败: remoteWorkId={}", remoteWorkId, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,4 +23,12 @@ public interface ILeaiSyncService {
|
|||||||
* @param source 来源标识(用于日志,如 "Webhook[work.status_changed]")
|
* @param source 来源标识(用于日志,如 "Webhook[work.status_changed]")
|
||||||
*/
|
*/
|
||||||
void syncWork(String remoteWorkId, Map<String, Object> remoteData, String source);
|
void syncWork(String remoteWorkId, Map<String, Object> remoteData, String source);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT 代理成功后,用本次请求体中的元数据直接覆盖本地 t_ugc_work(与乐读派查询解耦,避免远端滞后)。
|
||||||
|
*
|
||||||
|
* @param remoteWorkId 乐读派作品 ID
|
||||||
|
* @param proxyRequestBody 前端传入的 JSON(通常含 author、subtitle、intro、tags 等,不含 orgId/phone)
|
||||||
|
*/
|
||||||
|
void applyLocalMetadataFromProxyPut(String remoteWorkId, Map<String, Object> proxyRequestBody);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,6 +29,8 @@ import java.util.*;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class LeaiSyncService implements ILeaiSyncService {
|
public class LeaiSyncService implements ILeaiSyncService {
|
||||||
|
|
||||||
|
private static final String SOURCE_PROXY_UPDATE = "Proxy[update.work]";
|
||||||
|
|
||||||
private final UgcWorkMapper ugcWorkMapper;
|
private final UgcWorkMapper ugcWorkMapper;
|
||||||
private final UgcWorkPageMapper ugcWorkPageMapper;
|
private final UgcWorkPageMapper ugcWorkPageMapper;
|
||||||
private final LeaiApiClient leaiApiClient;
|
private final LeaiApiClient leaiApiClient;
|
||||||
@ -90,6 +92,16 @@ public class LeaiSyncService implements ILeaiSyncService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PUT /update/work 仅改作者/副标题/简介时乐读派 status 不变,不走「状态前进」;需单独同步元数据
|
||||||
|
if (remoteStatus >= LeaiCreationStatus.CATALOGED
|
||||||
|
&& remoteStatus == localLeaiStatus
|
||||||
|
&& source != null
|
||||||
|
&& source.contains(SOURCE_PROXY_UPDATE)) {
|
||||||
|
applyMetadataFromRemote(localWork, remoteData);
|
||||||
|
log.info("[{}] 元数据同步(状态未变) remoteWorkId={}", source, remoteWorkId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 旧数据或重复推送,忽略状态更新
|
// 旧数据或重复推送,忽略状态更新
|
||||||
// 但如果 remoteStatus >= 3 且本地缺少页面数据,需要补充拉取
|
// 但如果 remoteStatus >= 3 且本地缺少页面数据,需要补充拉取
|
||||||
if (remoteStatus >= LeaiCreationStatus.COMPLETED && !hasPages(localWork.getId())) {
|
if (remoteStatus >= LeaiCreationStatus.COMPLETED && !hasPages(localWork.getId())) {
|
||||||
@ -370,6 +382,94 @@ public class LeaiSyncService implements ILeaiSyncService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 乐读派编辑元数据后 status 不变时,将 title/author/副标题与简介 写入本地(无 leaiStatus CAS)
|
||||||
|
*/
|
||||||
|
private void applyMetadataFromRemote(UgcWork work, Map<String, Object> remoteData) {
|
||||||
|
LambdaUpdateWrapper<UgcWork> wrapper = new LambdaUpdateWrapper<>();
|
||||||
|
wrapper.eq(UgcWork::getId, work.getId());
|
||||||
|
boolean any = false;
|
||||||
|
if (remoteData.containsKey("title")) {
|
||||||
|
wrapper.set(UgcWork::getTitle, LeaiUtil.toString(remoteData.get("title"), null));
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
if (remoteData.containsKey("author")) {
|
||||||
|
wrapper.set(UgcWork::getAuthorName, LeaiUtil.toString(remoteData.get("author"), null));
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
if (remoteData.containsKey("intro") || remoteData.containsKey("subtitle")) {
|
||||||
|
String sub = remoteData.containsKey("subtitle")
|
||||||
|
? LeaiUtil.toString(remoteData.get("subtitle"), "")
|
||||||
|
: "";
|
||||||
|
String intro = remoteData.containsKey("intro")
|
||||||
|
? LeaiUtil.toString(remoteData.get("intro"), "")
|
||||||
|
: "";
|
||||||
|
String desc;
|
||||||
|
if (sub.isEmpty()) {
|
||||||
|
desc = intro;
|
||||||
|
} else if (intro.isEmpty()) {
|
||||||
|
desc = sub;
|
||||||
|
} else {
|
||||||
|
desc = sub + "\n" + intro;
|
||||||
|
}
|
||||||
|
desc = desc.trim();
|
||||||
|
wrapper.set(UgcWork::getDescription, desc.isEmpty() ? null : desc);
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
if (!any) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wrapper.set(UgcWork::getModifyTime, LocalDateTime.now());
|
||||||
|
ugcWorkMapper.update(null, wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void applyLocalMetadataFromProxyPut(String remoteWorkId, Map<String, Object> proxyRequestBody) {
|
||||||
|
if (remoteWorkId == null || remoteWorkId.isEmpty() || proxyRequestBody == null || proxyRequestBody.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UgcWork work = findByRemoteWorkId(remoteWorkId);
|
||||||
|
if (work == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LambdaUpdateWrapper<UgcWork> wrapper = new LambdaUpdateWrapper<>();
|
||||||
|
wrapper.eq(UgcWork::getId, work.getId());
|
||||||
|
boolean any = false;
|
||||||
|
if (proxyRequestBody.containsKey("author")) {
|
||||||
|
wrapper.set(UgcWork::getAuthorName, LeaiUtil.toString(proxyRequestBody.get("author"), null));
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
if (proxyRequestBody.containsKey("title")) {
|
||||||
|
wrapper.set(UgcWork::getTitle, LeaiUtil.toString(proxyRequestBody.get("title"), null));
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
if (proxyRequestBody.containsKey("intro") || proxyRequestBody.containsKey("subtitle")) {
|
||||||
|
String sub = proxyRequestBody.containsKey("subtitle")
|
||||||
|
? LeaiUtil.toString(proxyRequestBody.get("subtitle"), "")
|
||||||
|
: "";
|
||||||
|
String intro = proxyRequestBody.containsKey("intro")
|
||||||
|
? LeaiUtil.toString(proxyRequestBody.get("intro"), "")
|
||||||
|
: "";
|
||||||
|
String desc;
|
||||||
|
if (sub.isEmpty()) {
|
||||||
|
desc = intro;
|
||||||
|
} else if (intro.isEmpty()) {
|
||||||
|
desc = sub;
|
||||||
|
} else {
|
||||||
|
desc = sub + "\n" + intro;
|
||||||
|
}
|
||||||
|
desc = desc.trim();
|
||||||
|
wrapper.set(UgcWork::getDescription, desc.isEmpty() ? null : desc);
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
if (!any) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wrapper.set(UgcWork::getModifyTime, LocalDateTime.now());
|
||||||
|
ugcWorkMapper.update(null, wrapper);
|
||||||
|
log.info("[ProxyPut] 本地元数据已覆盖 remoteWorkId={}", remoteWorkId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过 remoteWorkId 查找本地作品
|
* 通过 remoteWorkId 查找本地作品
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -4,34 +4,35 @@
|
|||||||
<template #title>评委管理</template>
|
<template #title>评委管理</template>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-button
|
<a-button v-permission="'judge:create'" type="primary" @click="handleAdd">
|
||||||
v-permission="'judge:create'"
|
<template #icon>
|
||||||
type="primary"
|
<PlusOutlined />
|
||||||
@click="handleAdd"
|
</template>
|
||||||
>
|
|
||||||
<template #icon><PlusOutlined /></template>
|
|
||||||
新增
|
新增
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-tooltip title="功能开发中,敬请期待">
|
<a-tooltip title="功能开发中,敬请期待">
|
||||||
<a-button v-permission="'judge:create'" disabled>
|
<a-button v-permission="'judge:create'" disabled>
|
||||||
<template #icon><UploadOutlined /></template>
|
<template #icon>
|
||||||
|
<UploadOutlined />
|
||||||
|
</template>
|
||||||
导入
|
导入
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip title="功能开发中,敬请期待">
|
<a-tooltip title="功能开发中,敬请期待">
|
||||||
<a-button v-permission="'judge:read'" disabled>
|
<a-button v-permission="'judge:read'" disabled>
|
||||||
<template #icon><DownloadOutlined /></template>
|
<template #icon>
|
||||||
|
<DownloadOutlined />
|
||||||
|
</template>
|
||||||
导出
|
导出
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-popconfirm
|
<a-popconfirm v-permission="'judge:delete'" title="确定要删除选中的评委吗?"
|
||||||
v-permission="'judge:delete'"
|
|
||||||
title="确定要删除选中的评委吗?"
|
|
||||||
:disabled="selectedRowKeys.length === 0 || selectedRows.every(r => r.isPlatform)"
|
:disabled="selectedRowKeys.length === 0 || selectedRows.every(r => r.isPlatform)"
|
||||||
@confirm="handleBatchDelete"
|
@confirm="handleBatchDelete">
|
||||||
>
|
|
||||||
<a-button danger :disabled="selectedRowKeys.length === 0 || selectedRows.every(r => r.isPlatform)">
|
<a-button danger :disabled="selectedRowKeys.length === 0 || selectedRows.every(r => r.isPlatform)">
|
||||||
<template #icon><DeleteOutlined /></template>
|
<template #icon>
|
||||||
|
<DeleteOutlined />
|
||||||
|
</template>
|
||||||
删除
|
删除
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-popconfirm>
|
</a-popconfirm>
|
||||||
@ -40,70 +41,42 @@
|
|||||||
</a-card>
|
</a-card>
|
||||||
|
|
||||||
<!-- 搜索表单 -->
|
<!-- 搜索表单 -->
|
||||||
<a-form
|
<a-form :model="searchParams" layout="inline" class="search-form" @finish="handleSearch">
|
||||||
:model="searchParams"
|
|
||||||
layout="inline"
|
|
||||||
class="search-form"
|
|
||||||
@finish="handleSearch"
|
|
||||||
>
|
|
||||||
<a-form-item label="所属单位">
|
<a-form-item label="所属单位">
|
||||||
<a-input
|
<a-input v-model:value="searchParams.organization" placeholder="请输入所属单位" allow-clear style="width: 200px" />
|
||||||
v-model:value="searchParams.organization"
|
|
||||||
placeholder="请输入所属单位"
|
|
||||||
allow-clear
|
|
||||||
style="width: 200px"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="姓名">
|
<a-form-item label="姓名">
|
||||||
<a-input
|
<a-input v-model:value="searchParams.nickname" placeholder="请输入姓名" allow-clear style="width: 150px" />
|
||||||
v-model:value="searchParams.nickname"
|
|
||||||
placeholder="请输入姓名"
|
|
||||||
allow-clear
|
|
||||||
style="width: 150px"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="账号">
|
<a-form-item label="账号">
|
||||||
<a-input
|
<a-input v-model:value="searchParams.username" placeholder="请输入账号" allow-clear style="width: 150px" />
|
||||||
v-model:value="searchParams.username"
|
|
||||||
placeholder="请输入账号"
|
|
||||||
allow-clear
|
|
||||||
style="width: 150px"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="状态">
|
<a-form-item label="状态">
|
||||||
<a-select
|
<a-select v-model:value="searchParams.status" placeholder="请选择状态" allow-clear style="width: 120px"
|
||||||
v-model:value="searchParams.status"
|
@change="handleSearch">
|
||||||
placeholder="请选择状态"
|
|
||||||
allow-clear
|
|
||||||
style="width: 120px"
|
|
||||||
@change="handleSearch"
|
|
||||||
>
|
|
||||||
<a-select-option value="disabled">停用</a-select-option>
|
<a-select-option value="disabled">停用</a-select-option>
|
||||||
<a-select-option value="enabled">启用</a-select-option>
|
<a-select-option value="enabled">启用</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-button type="primary" html-type="submit">
|
<a-button type="primary" html-type="submit">
|
||||||
<template #icon><SearchOutlined /></template>
|
<template #icon>
|
||||||
|
<SearchOutlined />
|
||||||
|
</template>
|
||||||
搜索
|
搜索
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button style="margin-left: 8px" @click="handleReset">
|
<a-button style="margin-left: 8px" @click="handleReset">
|
||||||
<template #icon><ReloadOutlined /></template>
|
<template #icon>
|
||||||
|
<ReloadOutlined />
|
||||||
|
</template>
|
||||||
重置
|
重置
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
<!-- 数据表格 -->
|
<!-- 数据表格 -->
|
||||||
<a-table
|
<a-table :columns="columns" :data-source="dataSource" :loading="loading" :pagination="pagination"
|
||||||
:columns="columns"
|
:row-selection="rowSelection" row-key="id" @change="handleTableChange">
|
||||||
:data-source="dataSource"
|
|
||||||
:loading="loading"
|
|
||||||
:pagination="pagination"
|
|
||||||
:row-selection="rowSelection"
|
|
||||||
row-key="id"
|
|
||||||
@change="handleTableChange"
|
|
||||||
>
|
|
||||||
<template #bodyCell="{ column, record, index }">
|
<template #bodyCell="{ column, record, index }">
|
||||||
<template v-if="column.key === 'index'">
|
<template v-if="column.key === 'index'">
|
||||||
{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}
|
{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}
|
||||||
@ -126,9 +99,7 @@
|
|||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'ongoingContests'">
|
<template v-else-if="column.key === 'ongoingContests'">
|
||||||
<template
|
<template v-if="record.contestJudges && record.contestJudges.length > 0">
|
||||||
v-if="record.contestJudges && record.contestJudges.length > 0"
|
|
||||||
>
|
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template #title>
|
<template #title>
|
||||||
<div v-for="cj in record.contestJudges" :key="cj.contest.id">
|
<div v-for="cj in record.contestJudges" :key="cj.contest.id">
|
||||||
@ -145,31 +116,17 @@
|
|||||||
<template v-else-if="column.key === 'action'">
|
<template v-else-if="column.key === 'action'">
|
||||||
<a-space>
|
<a-space>
|
||||||
<template v-if="!record.isPlatform">
|
<template v-if="!record.isPlatform">
|
||||||
<a-button
|
<a-button v-permission="'judge:update'" type="link" size="small" @click="handleToggleStatus(record)">
|
||||||
v-permission="'judge:update'"
|
|
||||||
type="link"
|
|
||||||
size="small"
|
|
||||||
@click="handleToggleStatus(record)"
|
|
||||||
>
|
|
||||||
{{ record.status === "enabled" ? "冻结" : "解冻" }}
|
{{ record.status === "enabled" ? "冻结" : "解冻" }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="!record.isPlatform">
|
<template v-if="!record.isPlatform">
|
||||||
<a-button
|
<a-button v-permission="'judge:update'" type="link" size="small" @click="handleEdit(record)">
|
||||||
v-permission="'judge:update'"
|
|
||||||
type="link"
|
|
||||||
size="small"
|
|
||||||
@click="handleEdit(record)"
|
|
||||||
>
|
|
||||||
编辑
|
编辑
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
<a-popconfirm
|
<a-popconfirm v-if="!record.isPlatform" v-permission="'judge:delete'" title="确定要删除这个评委吗?"
|
||||||
v-if="!record.isPlatform"
|
@confirm="handleDelete(record.id)">
|
||||||
v-permission="'judge:delete'"
|
|
||||||
title="确定要删除这个评委吗?"
|
|
||||||
@confirm="handleDelete(record.id)"
|
|
||||||
>
|
|
||||||
<a-button type="link" danger size="small">删除</a-button>
|
<a-button type="link" danger size="small">删除</a-button>
|
||||||
</a-popconfirm>
|
</a-popconfirm>
|
||||||
</a-space>
|
</a-space>
|
||||||
@ -178,34 +135,14 @@
|
|||||||
</a-table>
|
</a-table>
|
||||||
|
|
||||||
<!-- 新增/编辑评委抽屉 -->
|
<!-- 新增/编辑评委抽屉 -->
|
||||||
<a-drawer
|
<a-drawer v-model:open="drawerVisible" :title="isEditing ? '编辑评委' : '新增评委'" placement="right" width="500px"
|
||||||
v-model:open="drawerVisible"
|
:footer-style="{ textAlign: 'right' }">
|
||||||
:title="isEditing ? '编辑评委' : '新增评委'"
|
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
|
||||||
placement="right"
|
|
||||||
width="500px"
|
|
||||||
:footer-style="{ textAlign: 'right' }"
|
|
||||||
>
|
|
||||||
<a-form
|
|
||||||
ref="formRef"
|
|
||||||
:model="form"
|
|
||||||
:rules="rules"
|
|
||||||
:label-col="{ span: 6 }"
|
|
||||||
:wrapper-col="{ span: 18 }"
|
|
||||||
>
|
|
||||||
<a-form-item label="账号" name="username">
|
<a-form-item label="账号" name="username">
|
||||||
<a-input
|
<a-input v-model:value="form.username" placeholder="请输入账号" :maxlength="50" :disabled="isEditing" />
|
||||||
v-model:value="form.username"
|
|
||||||
placeholder="请输入账号"
|
|
||||||
:maxlength="50"
|
|
||||||
:disabled="isEditing"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="姓名" name="nickname">
|
<a-form-item label="姓名" name="nickname">
|
||||||
<a-input
|
<a-input v-model:value="form.nickname" placeholder="请输入姓名" :maxlength="50" />
|
||||||
v-model:value="form.nickname"
|
|
||||||
placeholder="请输入姓名"
|
|
||||||
:maxlength="50"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="性别" name="gender">
|
<a-form-item label="性别" name="gender">
|
||||||
<a-radio-group v-model:value="form.gender">
|
<a-radio-group v-model:value="form.gender">
|
||||||
@ -214,31 +151,18 @@
|
|||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="所属单位" name="organization">
|
<a-form-item label="所属单位" name="organization">
|
||||||
<a-input
|
<a-input v-model:value="form.organization" placeholder="请输入所属单位" :maxlength="100" />
|
||||||
v-model:value="form.organization"
|
|
||||||
placeholder="请输入所属单位"
|
|
||||||
:maxlength="100"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="联系方式" name="phone">
|
<a-form-item label="联系方式" name="phone">
|
||||||
<a-input
|
<a-input v-model:value="form.phone" placeholder="请输入手机号" :maxlength="11" />
|
||||||
v-model:value="form.phone"
|
|
||||||
placeholder="请输入手机号"
|
|
||||||
:maxlength="11"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="初始密码" name="password">
|
<a-form-item label="初始密码" name="password">
|
||||||
<a-input-password
|
<a-input-password v-model:value="form.password" :placeholder="isEditing ? '请输入新密码' : '请输入初始密码'"
|
||||||
v-model:value="form.password"
|
:maxlength="50" />
|
||||||
:placeholder="isEditing ? '请输入新密码' : '请输入初始密码'"
|
|
||||||
:maxlength="50"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<a-button style="margin-right: 8px" @click="handleCancel"
|
<a-button style="margin-right: 8px" @click="handleCancel">取消</a-button>
|
||||||
>取消</a-button
|
|
||||||
>
|
|
||||||
<a-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
<a-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||||
确定
|
确定
|
||||||
</a-button>
|
</a-button>
|
||||||
@ -562,7 +486,7 @@ const handleSubmit = async () => {
|
|||||||
}
|
}
|
||||||
message.error(
|
message.error(
|
||||||
error?.response?.data?.message ||
|
error?.response?.data?.message ||
|
||||||
(isEditing.value ? "编辑失败" : "创建失败")
|
(isEditing.value ? "编辑失败" : "创建失败")
|
||||||
)
|
)
|
||||||
} finally {
|
} finally {
|
||||||
submitLoading.value = false
|
submitLoading.value = false
|
||||||
@ -592,9 +516,16 @@ $primary: #6366f1;
|
|||||||
|
|
||||||
.ant-card-head {
|
.ant-card-head {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
.ant-card-head-title { font-size: 18px; font-weight: 600; }
|
|
||||||
|
.ant-card-head-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
.ant-card-body { padding: 0; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.ant-table-wrapper) {
|
:deep(.ant-table-wrapper) {
|
||||||
@ -603,9 +534,19 @@ $primary: #6366f1;
|
|||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.ant-table-thead > tr > th { background: #fafafa; font-weight: 600; }
|
.ant-table-thead>tr>th {
|
||||||
.ant-table-tbody > tr:hover > td { background: rgba($primary, 0.03); }
|
background: #fafafa;
|
||||||
.ant-table-pagination { padding: 16px; margin: 0; }
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody>tr:hover>td {
|
||||||
|
background: rgba($primary, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-pagination {
|
||||||
|
padding: 16px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -517,8 +517,11 @@ async function loadWork() {
|
|||||||
}
|
}
|
||||||
const w = store.workDetail
|
const w = store.workDetail
|
||||||
|
|
||||||
if (w.status >= STATUS.DUBBED) {
|
// 仅当尚未编目完成(CATALOGED)时按流程跳转(如 COMPLETED→预览);已 DUBBED 仍允许停留本页,
|
||||||
const nextRoute = getRouteByStatus(w.status, w.workId)
|
// 否则从编辑页点「去配音」会立刻被 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 }
|
if (nextRoute) { router.replace(nextRoute); return }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -115,7 +115,7 @@ export default { name: 'EditInfoView' }
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onActivated, nextTick } from 'vue'
|
import { ref, computed, onActivated, nextTick, watch } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
LoadingOutlined,
|
LoadingOutlined,
|
||||||
@ -142,6 +142,13 @@ const route = useRoute()
|
|||||||
const store = useAicreateStore()
|
const store = useAicreateStore()
|
||||||
const workId = computed(() => route.params.workId || store.workId)
|
const workId = computed(() => route.params.workId || store.workId)
|
||||||
|
|
||||||
|
/** 乐读派 workId 可能超过 JS 安全整数,请求与比较一律用路由字符串 */
|
||||||
|
function resolvedWorkIdStr() {
|
||||||
|
const id = workId.value
|
||||||
|
if (id == null || id === '') return ''
|
||||||
|
return String(id)
|
||||||
|
}
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const processing = ref(false)
|
const processing = ref(false)
|
||||||
const coverUrl = ref('')
|
const coverUrl = ref('')
|
||||||
@ -182,19 +189,22 @@ function confirmAddTag() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadWork() {
|
async function loadWork() {
|
||||||
|
const id = resolvedWorkIdStr()
|
||||||
|
if (!id) {
|
||||||
|
loading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
// 缓存不匹配当前 workId 时重新请求
|
// 必须每次拉取详情:创作壳 keep-alive 会缓存组件,仅靠 store 命中会跳过请求,二次进入表单仍是旧值
|
||||||
if (!store.workDetail || store.workDetail.workId !== workId.value) {
|
store.workDetail = null
|
||||||
store.workDetail = null
|
const res = await getWorkDetail(id)
|
||||||
const res = await getWorkDetail(workId.value)
|
store.workDetail = res
|
||||||
store.workDetail = res
|
|
||||||
}
|
|
||||||
const w = store.workDetail
|
const w = store.workDetail
|
||||||
|
|
||||||
// 已配音(DUBBED)仍可在本页编辑元数据/发布;仅当状态高于当前流程终态时再按 status 跳转
|
// 已配音(DUBBED)仍可在本页编辑元数据/发布;仅当状态高于当前流程终态时再按 status 跳转
|
||||||
if (w.status > STATUS.DUBBED) {
|
if (w.status > STATUS.DUBBED) {
|
||||||
const nextRoute = getRouteByStatus(w.status, w.workId)
|
const nextRoute = getRouteByStatus(w.status, id)
|
||||||
if (nextRoute) { router.replace(nextRoute); return }
|
if (nextRoute) { router.replace(nextRoute); return }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,24 +237,29 @@ function validate() {
|
|||||||
*/
|
*/
|
||||||
async function saveFormToServer() {
|
async function saveFormToServer() {
|
||||||
try {
|
try {
|
||||||
|
const id = resolvedWorkIdStr()
|
||||||
|
if (!id) {
|
||||||
|
message.error('作品 ID 无效')
|
||||||
|
return false
|
||||||
|
}
|
||||||
const data = { tags: selectedTags.value }
|
const data = { tags: selectedTags.value }
|
||||||
data.author = form.value.author.trim()
|
data.author = form.value.author.trim()
|
||||||
if (form.value.subtitle.trim()) data.subtitle = form.value.subtitle.trim()
|
data.subtitle = form.value.subtitle.trim()
|
||||||
if (form.value.intro.trim()) data.intro = form.value.intro.trim()
|
data.intro = form.value.intro.trim()
|
||||||
|
|
||||||
await updateWork(workId.value, data)
|
await updateWork(id, data)
|
||||||
|
|
||||||
if (store.workDetail) {
|
if (store.workDetail) {
|
||||||
store.workDetail.author = data.author
|
store.workDetail.author = data.author
|
||||||
if (data.subtitle) store.workDetail.subtitle = data.subtitle
|
store.workDetail.subtitle = data.subtitle
|
||||||
if (data.intro) store.workDetail.intro = data.intro
|
store.workDetail.intro = data.intro
|
||||||
store.workDetail.tags = [...selectedTags.value]
|
store.workDetail.tags = [...selectedTags.value]
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 容错:保存报错时检查实际状态,可能已经成功但重试导致 CAS 失败
|
// 容错:保存报错时检查实际状态,可能已经成功但重试导致 CAS 失败
|
||||||
try {
|
try {
|
||||||
const check = await getWorkDetail(workId.value)
|
const check = await getWorkDetail(resolvedWorkIdStr())
|
||||||
if (check?.status >= 4) return true
|
if (check?.status >= 4) return true
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
message.error(e.message || '保存失败,请重试')
|
message.error(e.message || '保存失败,请重试')
|
||||||
@ -261,7 +276,7 @@ async function handleSave() {
|
|||||||
store.workDetail = null
|
store.workDetail = null
|
||||||
router.push({
|
router.push({
|
||||||
name: 'PublicCreateSaveSuccess',
|
name: 'PublicCreateSaveSuccess',
|
||||||
params: { workId: String(workId.value || '') },
|
params: { workId: resolvedWorkIdStr() },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@ -276,7 +291,7 @@ async function handleGoDubbing() {
|
|||||||
try {
|
try {
|
||||||
if (await saveFormToServer()) {
|
if (await saveFormToServer()) {
|
||||||
store.workDetail = null
|
store.workDetail = null
|
||||||
router.push(`/p/create/dubbing/${workId.value}`)
|
router.push(`/p/create/dubbing/${resolvedWorkIdStr()}`)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
processing.value = false
|
processing.value = false
|
||||||
@ -308,7 +323,7 @@ async function handlePublish() {
|
|||||||
try {
|
try {
|
||||||
if (!(await saveFormToServer())) return
|
if (!(await saveFormToServer())) return
|
||||||
|
|
||||||
const rw = String(workId.value || '').trim()
|
const rw = resolvedWorkIdStr().trim()
|
||||||
let ugcId = await findUgcIdByRemoteWorkId(rw)
|
let ugcId = await findUgcIdByRemoteWorkId(rw)
|
||||||
if (ugcId == null) {
|
if (ugcId == null) {
|
||||||
message.error('作品库尚未同步该绘本,请稍后重试或到作品详情页点击「公开发布」')
|
message.error('作品库尚未同步该绘本,请稍后重试或到作品详情页点击「公开发布」')
|
||||||
@ -341,6 +356,15 @@ async function handlePublish() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 同组件仅路由参数切换作品时(keep-alive 复用实例)需重新拉详情
|
||||||
|
watch(
|
||||||
|
() => String(route.params.workId || ''),
|
||||||
|
(newId, oldId) => {
|
||||||
|
if (!newId) return
|
||||||
|
if (oldId !== undefined && newId !== oldId) loadWork()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
clearExtractDraft()
|
clearExtractDraft()
|
||||||
loadWork()
|
loadWork()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user