feat(publish-dedup): disable already-published accounts and rework retry UX

In the admin publish modal, fetch the article's publish records, disable
accounts that already succeeded for the same article/platform, label
queued/publishing accounts inline, and route the success notification by
created vs existing task counts (with a dedicated warning for the
`desktop_publish_duplicate` server error).

Replace the "再次发送" mutation/popconfirm on both admin and desktop publish
management views with a "重新创建发布任务" action that calls the same Create
endpoint, only enables on failed/aborted/unknown rows, and reports created
vs existing task counts (with a tailored warning for `unknown` results
that may have already succeeded).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 18:43:42 +08:00
parent 24dd832218
commit aff2c5f6ea
6 changed files with 311 additions and 107 deletions
@@ -60,7 +60,7 @@ const navItems = computed(() => {
{
to: '/publish-management',
title: '发布管理',
description: '查看待发布队列、历史发送结果,并对文章再次发送',
description: '查看待发布队列、历史发送结果和账号工作台',
count: data?.summary.queuedTasks ?? 0,
icon: SendOutlined,
},
@@ -5,8 +5,8 @@ import {
DesktopOutlined,
ExportOutlined,
ReloadOutlined,
RetweetOutlined,
SearchOutlined,
SendOutlined,
} from '@ant-design/icons-vue'
import type { DesktopPublishTaskListResponse, DesktopTaskInfo, JsonValue } from '@geo/shared-types'
import { notification } from 'ant-design-vue'
@@ -302,12 +302,12 @@ function summaryForTask(
}
return '文章已成功发送到目标平台。'
case 'failed':
return '本次发送失败,可以手动再次发送。'
return '本次发送失败,请检查账号状态或平台提示后重新创建发布。'
case 'aborted':
return '任务已被取消,如需继续可再次发送。'
return '任务已被取消,请检查原因后重新创建发布。'
case 'unknown':
default:
return '发送结果异常,已按失败处理,检查平台侧结果后再次发送。'
return '发送结果异常,已按失败处理,检查平台侧结果后重新创建发布。'
}
}
@@ -326,9 +326,9 @@ const searchTitle = ref('')
const appliedTitle = ref('')
const loading = ref(false)
const error = ref<string | null>(null)
const actionPendingTaskId = ref<string | null>(null)
const consolePendingTaskId = ref<string | null>(null)
const copiedErrorTaskId = ref<string | null>(null)
const retryingTaskId = ref<string | null>(null)
function notifySuccess(message: string) {
notification.success({
@@ -422,22 +422,6 @@ function unbindPublishLeaseListener() {
runtimeInvalidationUnsubscribe = null
}
async function retryTask(taskId: string) {
actionPendingTaskId.value = taskId
try {
await window.desktopBridge.app.retryPublishTask(taskId)
notifySuccess('已重新加入发布队列')
await refreshTasks()
} catch (err) {
notifyActionError(
normalizeTaskErrorMessage(unwrapClientErrorMessage(err, '重新发送失败')) ?? '重新发送失败',
)
} finally {
actionPendingTaskId.value = null
}
}
async function openExternalUrl(url: string | null) {
if (!url) {
return
@@ -473,6 +457,39 @@ async function openAccountWorkbench(record: PublishTaskItem) {
}
}
function canRetryTask(record: PublishTaskItem): boolean {
return record.status === 'failed' || record.status === 'aborted' || record.status === 'unknown'
}
async function retryTask(record: PublishTaskItem) {
retryingTaskId.value = record.id
try {
const result = await window.desktopBridge.app.retryPublishTask(record.id)
const created = result.created_task_ids.length
const existing = result.existing_task_ids.length
if (created > 0) {
notifySuccess(created === 1 ? '已重新创建发布任务' : `已重新创建 ${created} 个发布任务`)
} else if (existing > 0) {
if (record.status === 'unknown') {
notifyActionError('原任务结果未知且已有发布记录,可能已成功,请先核实平台侧结果。')
} else {
notifySuccess('发布任务已存在,无需重复创建')
}
} else {
notifySuccess('已提交重试请求')
}
await refreshTasks(1)
} catch (err) {
notifyActionError(
normalizeTaskErrorMessage(unwrapClientErrorMessage(err, '重新创建发布任务失败')) ??
'重新创建发布任务失败',
)
} finally {
retryingTaskId.value = null
}
}
async function copyTextToClipboard(text: string): Promise<void> {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text)
@@ -846,26 +863,18 @@ const tableScroll = { x: 1080 } as const
<template #icon><DesktopOutlined /></template>
</a-button>
</a-tooltip>
<a-popconfirm
v-if="!isPendingStatus(record.status)"
title="确定要重试本次发布吗?"
placement="topRight"
ok-text="确定"
cancel-text="取消"
@confirm="retryTask(record.id)"
>
<a-tooltip title="再次发送" placement="top">
<a-button
type="text"
class="icon-action-btn"
:loading="actionPendingTaskId === record.id"
:aria-label="`再次发送:${record.title}`"
>
<template #icon><SendOutlined /></template>
</a-button>
</a-tooltip>
</a-popconfirm>
<a-tooltip v-else title="排队中...">
<a-tooltip v-if="canRetryTask(record)" title="重新创建发布任务" placement="top">
<a-button
type="text"
class="icon-action-btn"
:loading="retryingTaskId === record.id"
:aria-label="`重新创建发布任务:${record.title}`"
@click.stop="retryTask(record)"
>
<template #icon><RetweetOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip v-if="isPendingStatus(record.status)" title="排队中...">
<div class="action-placeholder-icon">
<ClockCircleOutlined />
</div>