Preserve publish record history
Frontend CI / Frontend (push) Successful in 3m15s
Backend CI / Backend (push) Successful in 14m44s

This commit is contained in:
2026-06-25 01:52:26 +08:00
parent 24a62fd52c
commit bdfe79c921
8 changed files with 162 additions and 52 deletions
@@ -112,6 +112,12 @@ const publishRecordColumns = computed<TableColumnsType<PublishRecord>>(() => [
key: 'published_at',
width: 168,
},
{
title: '错误信息',
dataIndex: 'error_message',
key: 'error_message',
width: 220,
},
{
title: t('media.records.link'),
dataIndex: 'external_article_url',
@@ -249,6 +255,18 @@ function shouldShowClientManageHint(record: PublishRecord): boolean {
)
}
function publishRecordError(record: PublishRecord): string {
return record.error_message?.trim() || ''
}
function publishRecordErrorPreview(record: PublishRecord): string {
const error = publishRecordError(record).replace(/\s+/g, ' ')
if (error.length <= 52) {
return error
}
return `${error.slice(0, 44)}...${error.slice(-5)}`
}
async function copyPublishLink(record: PublishRecord): Promise<void> {
const target = resolvePublishLink(record)
if (!target) {
@@ -399,6 +417,14 @@ async function copyPublishLink(record: PublishRecord): Promise<void> {
<template v-else-if="column.key === 'published_at'">
{{ formatDateTime(record.published_at) }}
</template>
<template v-else-if="column.key === 'error_message'">
<a-tooltip v-if="publishRecordError(record)" :title="publishRecordError(record)">
<span class="article-drawer__record-error">
{{ publishRecordErrorPreview(record) }}
</span>
</a-tooltip>
<span v-else class="article-drawer__record-empty">--</span>
</template>
<template v-else-if="column.key === 'external_article_url'">
<div v-if="resolvePublishLink(record)" class="article-drawer__record-link-row">
<a
@@ -567,6 +593,18 @@ async function copyPublishLink(record: PublishRecord): Promise<void> {
font-size: 12px;
}
.article-drawer__record-error {
display: inline-block;
max-width: 190px;
overflow: hidden;
color: #cf1322;
font-size: 12px;
line-height: 1.5;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
}
.article-drawer__record-link {
flex: 1;
min-width: 0;
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { QuestionCircleOutlined, UserOutlined } from '@ant-design/icons-vue'
import type { MediaPlatform } from '@geo/shared-types'
import type { MediaPlatform, PublishRecord } from '@geo/shared-types'
import { useQuery } from '@tanstack/vue-query'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -80,15 +80,21 @@ const platformMap = computed(() => {
})
const platformEntries = computed<PublishPlatformEntry[]>(() => {
const entries: PublishPlatformEntry[] = []
const entriesByTarget = new Map<string, PublishPlatformEntry>()
const latestRecords = [...(publishRecordsQuery.data.value ?? [])].sort(comparePublishRecordsDesc)
for (const record of publishRecordsQuery.data.value ?? []) {
for (const record of latestRecords) {
const targetType = String(record.target_type ?? 'platform_account')
const normalizedId = normalizePublishPlatformId(record.platform_id)
const platform = platformMap.value.get(normalizedId)
const fallback = enterprisePlatformFallback(normalizedId)
const targetKey = publishRecordTargetKey(record, targetType, normalizedId)
entries.push({
if (entriesByTarget.has(targetKey)) {
continue
}
entriesByTarget.set(targetKey, {
key: `${targetType}:${record.id}`,
name: record.platform_name || platform?.name || fallback.name,
shortName: platform?.short_name || fallback.shortName,
@@ -100,7 +106,7 @@ const platformEntries = computed<PublishPlatformEntry[]>(() => {
})
}
return entries
return [...entriesByTarget.values()]
})
const successEntries = computed(() =>
@@ -224,6 +230,45 @@ function normalizePublishRecordStatus(status?: string | null): PublishRecordStat
}
}
function publishRecordTargetKey(
record: PublishRecord,
targetType: string,
normalizedPlatformId: string,
): string {
if (targetType === 'enterprise_site') {
return `${targetType}:${record.target_connection_id ?? normalizedPlatformId}`
}
const accountKey =
normalizeTargetKeyPart(record.platform_uid) ||
normalizeTargetKeyPart(record.desktop_account_id) ||
normalizeTargetKeyPart(record.platform_account_id) ||
normalizeTargetKeyPart(record.platform_nickname) ||
'unknown'
return `${targetType}:${normalizedPlatformId}:${accountKey}`
}
function normalizeTargetKeyPart(value: unknown): string {
return String(value ?? '').trim()
}
function comparePublishRecordsDesc(
a: PublishRecord,
b: PublishRecord,
): number {
const aTime = Date.parse(a.created_at || a.updated_at || '')
const bTime = Date.parse(b.created_at || b.updated_at || '')
const normalizedATime = Number.isFinite(aTime) ? aTime : 0
const normalizedBTime = Number.isFinite(bTime) ? bTime : 0
if (normalizedATime !== normalizedBTime) {
return normalizedBTime - normalizedATime
}
return Number(b.id ?? 0) - Number(a.id ?? 0)
}
function getAccountInitial(accountName: string): string {
const trimmed = String(accountName ?? '').trim()
if (!trimmed || trimmed === '--') {
@@ -2,7 +2,6 @@
import {
ClockCircleOutlined,
CopyOutlined,
DeleteOutlined,
DesktopOutlined,
ExportOutlined,
ReloadOutlined,
@@ -194,7 +193,6 @@ const appliedTitle = ref('')
const copiedErrorTaskId = ref<string | null>(null)
const retryingTaskId = ref<string | null>(null)
const cancelingTaskId = ref<string | null>(null)
const deletingRecordId = ref<string | null>(null)
const manualRefreshing = ref(false)
const recordsQuery = useQuery({
@@ -372,22 +370,6 @@ async function cancelTask(record: PublishRecordItem): Promise<void> {
}
}
async function deleteRecord(record: PublishRecordItem): Promise<void> {
deletingRecordId.value = record.id
try {
await publishRecordsApi.remove(record.recordId)
message.success('发布记录已删除')
if (publishRecords.value.length === 1 && page.value > 1) {
page.value -= 1
}
await recordsQuery.refetch()
} catch (error) {
message.error(formatError(error))
} finally {
deletingRecordId.value = null
}
}
async function copyErrorMessage(record: PublishRecordItem): Promise<void> {
if (!record.errorMessage) {
return
@@ -640,25 +622,6 @@ async function copyErrorMessage(record: PublishRecordItem): Promise<void> {
<ClockCircleOutlined />
</span>
</a-tooltip>
<a-popconfirm
title="确认删除这条发布记录?"
ok-text="删除"
cancel-text="取消"
placement="topRight"
@confirm="deleteRecord(record)"
>
<a-tooltip title="删除发布记录" placement="top">
<a-button
type="text"
class="publish-action-btn publish-action-btn--danger"
:loading="deletingRecordId === record.id"
:aria-label="`删除发布记录:${record.title}`"
@click.stop
>
<template #icon><DeleteOutlined /></template>
</a-button>
</a-tooltip>
</a-popconfirm>
</div>
</template>
</template>