feat(ui/publish-records): surface 已存草稿 status and client-only manage hint

Render publish_type=draft tasks with the warn tone and 已存草稿 label, swap
the management link copy to 打开草稿箱 for weixin_gzh, and expand the task
summary to mention the publish_authorization_required reason. In the admin
drawer, stop falling back to external_manage_url for missing public links
and instead show a hint pointing operators back to the desktop client for
weixin_gzh / zol records.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-29 19:57:11 +08:00
parent a09d44d8f0
commit 99a992f34b
3 changed files with 60 additions and 6 deletions
@@ -229,7 +229,13 @@ async function refreshArticleData(): Promise<void> {
}
function resolvePublishLink(record: PublishRecord): string {
return record.external_article_url || record.external_manage_url || "";
return record.external_article_url?.trim() || "";
}
function shouldShowClientManageHint(record: PublishRecord): boolean {
return !resolvePublishLink(record)
&& Boolean(record.external_manage_url?.trim())
&& ["weixin_gzh", "zol"].includes(record.platform_id);
}
async function copyPublishLink(record: PublishRecord): Promise<void> {
@@ -395,6 +401,9 @@ async function copyPublishLink(record: PublishRecord): Promise<void> {
</a-button>
</a-tooltip>
</div>
<span v-else-if="shouldShowClientManageHint(record)" class="article-drawer__record-hint">
{{ t("media.records.clientManageHint") }}
</span>
<span v-else class="article-drawer__record-empty">--</span>
</template>
</template>
@@ -521,6 +530,11 @@ async function copyPublishLink(record: PublishRecord): Promise<void> {
font-size: 12px;
}
.article-drawer__record-hint {
color: var(--muted);
font-size: 12px;
}
.article-drawer__record-link {
flex: 1;
min-width: 0;
@@ -1086,6 +1086,7 @@ const zhCN = {
copy: "复制",
empty: "暂无发布记录",
linkEmpty: "当前记录还没有可用链接",
clientManageHint: "仅有管理页面,请在客户端打开管理页面查看",
copySuccess: "链接已复制",
},
},
@@ -41,6 +41,8 @@ interface PublishTaskItem {
updatedAt: number;
externalArticleUrl: string | null;
externalManageUrl: string | null;
publishType: string | null;
fallbackReason: string | null;
errorMessage: string | null;
}
@@ -79,6 +81,13 @@ function statusLabel(status: DesktopTaskInfo["status"]): string {
return map[status];
}
function statusLabelForTask(task: PublishTaskItem): string {
if (task.status === "succeeded" && task.publishType === "draft") {
return "已存草稿";
}
return statusLabel(task.status);
}
function isPendingStatus(status: DesktopTaskInfo["status"]): boolean {
return status === "queued" || status === "in_progress";
}
@@ -98,6 +107,13 @@ function statusTone(status: DesktopTaskInfo["status"]): "info" | "warn" | "succe
}
}
function statusToneForTask(task: PublishTaskItem): "info" | "warn" | "success" | "danger" {
if (task.status === "succeeded" && task.publishType === "draft") {
return "warn";
}
return statusTone(task.status);
}
function extractNumber(value: JsonValue | null | undefined): number | null {
if (typeof value === "number" && Number.isFinite(value)) {
return value;
@@ -164,13 +180,23 @@ function inlineErrorPreview(message: string): string {
return `${compact.slice(0, INLINE_ERROR_HEAD_CHARS)}...${compact.slice(-INLINE_ERROR_TAIL_CHARS)}`;
}
function summaryForTask(status: DesktopTaskInfo["status"]): string {
function summaryForTask(
status: DesktopTaskInfo["status"],
publishType: string | null = null,
fallbackReason: string | null = null,
): string {
switch (status) {
case "queued":
return "任务已进入队列,等前面的 PublishTasks 翻完后再继续展示历史记录。";
case "in_progress":
return "当前正在由桌面端执行发送,请避免重复触发。";
case "succeeded":
if (publishType === "draft") {
if (fallbackReason === "publish_authorization_required") {
return "草稿已保存,需在公众号后台完成微信验证/授权发布。";
}
return "文章已保存到草稿箱,需在平台后台完成发表。";
}
return "文章已成功发送到目标平台。";
case "failed":
return "本次发送失败,可以手动再次发送。";
@@ -182,6 +208,13 @@ function summaryForTask(status: DesktopTaskInfo["status"]): string {
}
}
function manageLinkLabel(task: PublishTaskItem): string {
if (task.publishType === "draft" && task.platform === "weixin_gzh") {
return "打开草稿箱";
}
return "打开管理页";
}
function parseTimestamp(value: string | null): number {
if (!value) {
return Date.now();
@@ -376,6 +409,8 @@ const publishTasks = computed<PublishTaskItem[]>(() =>
const payload = task.payload ?? {};
const result = task.result ?? {};
const taskError = task.error ?? {};
const publishType = extractString(result.publish_type);
const fallbackReason = extractString(result.fallback_reason);
const nestedContentRef =
payload.content_ref && typeof payload.content_ref === "object" && !Array.isArray(payload.content_ref)
? (payload.content_ref as Record<string, JsonValue>)
@@ -390,13 +425,17 @@ const publishTasks = computed<PublishTaskItem[]>(() =>
accountId: task.target_account_id,
accountName: accountNameMap.value.get(task.target_account_id) ?? "待同步账号",
status: normalizedStatus,
summary: summaryForTask(normalizedStatus),
summary: summaryForTask(normalizedStatus, publishType, fallbackReason),
attempts: task.attempts,
leaseExpiresAt: task.lease_expires_at ? parseTimestamp(task.lease_expires_at) : null,
createdAt: parseTimestamp(task.created_at),
updatedAt: parseTimestamp(task.updated_at),
externalArticleUrl: extractString(result.external_article_url) ?? buildSohuArticleUrl(task, result),
externalArticleUrl: publishType === "draft"
? null
: extractString(result.external_article_url) ?? buildSohuArticleUrl(task, result),
externalManageUrl: extractString(result.external_manage_url),
publishType,
fallbackReason,
errorMessage: normalizeTaskErrorMessage(
extractString(taskError.message)
?? extractString(taskError.detail)
@@ -533,7 +572,7 @@ const tableScroll = { x: 1000 } as const;
</template>
<template v-else-if="column.key === 'status'">
<StatusBadge :tone="statusTone(record.status)" :label="statusLabel(record.status)" />
<StatusBadge :tone="statusToneForTask(record)" :label="statusLabelForTask(record)" />
</template>
<template v-else-if="column.key === 'meta'">
@@ -592,7 +631,7 @@ const tableScroll = { x: 1000 } as const;
@click.prevent.stop="openAccountWorkbench(record)"
>
<LinkOutlined />
打开管理页
{{ manageLinkLabel(record) }}
</a>
</a-tooltip>
</template>