refactor(publish): redesign management list with hero card and sohu URL fallback

Switch the publish management page to the hero-card + stats-strip layout
shared with AccountsView for visual consistency: bigger title, stats
row, modern table styling, and a content-section wrapper. Add a
buildSohuArticleUrl helper that reconstructs the sohu article URL from
the task result and the bound account's platform uid when the adapter
result didn't include external_article_url, so older finished tasks
also resolve a viewable link.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-27 20:07:10 +08:00
parent def2db5902
commit 9480192d66
@@ -116,6 +116,27 @@ function extractString(value: JsonValue | null | undefined): string | null {
return trimmed ? trimmed : null;
}
function extractStringId(value: JsonValue | null | undefined): string | null {
if (typeof value === "number" && Number.isFinite(value)) {
return String(Math.trunc(value));
}
return extractString(value);
}
function buildSohuArticleUrl(task: DesktopTaskInfo, result: Record<string, JsonValue>): string | null {
if (task.platform !== "sohuhao" || extractString(result.publish_type) === "draft") {
return null;
}
const articleId = extractStringId(result.external_article_id);
const accountUid = accountMap.value.get(task.target_account_id)?.platformUid?.trim() ?? "";
if (!articleId || !accountUid) {
return null;
}
return `https://www.sohu.com/a/${encodeURIComponent(articleId)}_${encodeURIComponent(accountUid)}`;
}
function normalizePublishTaskStatus(status: DesktopTaskInfo["status"]): DesktopTaskInfo["status"] {
return status === "unknown" ? "failed" : status;
}
@@ -374,7 +395,7 @@ const publishTasks = computed<PublishTaskItem[]>(() =>
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),
externalArticleUrl: extractString(result.external_article_url) ?? buildSohuArticleUrl(task, result),
externalManageUrl: extractString(result.external_manage_url),
errorMessage: normalizeTaskErrorMessage(
extractString(taskError.message)
@@ -403,66 +424,65 @@ const tableScroll = { x: 1000 } as const;
<template>
<section class="page-container">
<header class="page-header">
<div class="page-header__title">
<p class="eyebrow">Publish Execution Desk</p>
<h2>发布管理</h2>
</div>
<div class="page-header__metrics" role="group" aria-label="发布任务概览">
<div class="metric" :class="{ 'metric--accent': pendingCount > 0 }">
<span class="metric__label">等待发布</span>
<span class="metric__value">{{ pendingCount }}</span>
<section class="hero-card">
<div class="hero-header">
<div class="hero-title">
<p class="eyebrow">PUBLISH EXECUTION DESK</p>
<h2>发布管理</h2>
</div>
<div class="metric">
<span class="metric__label">已发送</span>
<span class="metric__value">{{ historyTotal }}</span>
</div>
<div class="metric metric--total">
<span class="metric__label">历史总数</span>
<span class="metric__value">{{ totalCount }}</span>
<div class="hero-actions">
<a-button type="primary" ghost class="modern-btn" :loading="loading" @click="refreshTasks()">
<template #icon><ReloadOutlined /></template>
刷新状态
</a-button>
</div>
</div>
<div class="page-header__actions">
<a-button class="ghost-btn" :loading="loading" @click="refreshTasks()">
<template #icon><ReloadOutlined /></template>
刷新
</a-button>
<div class="stats-strip">
<div class="stat-card">
<div class="stat-label">等待发布</div>
<div class="stat-value" :class="{ 'stat-value--accent': pendingCount > 0 }">{{ pendingCount }}</div>
</div>
<div class="stat-card">
<div class="stat-label">已发送</div>
<div class="stat-value">{{ historyTotal }}</div>
</div>
<div class="stat-card">
<div class="stat-label">历史总数</div>
<div class="stat-value">{{ totalCount }}</div>
</div>
</div>
</header>
<p class="page-summary">
待发布任务始终排在最前每页 10 当前页 PublishTasks 展示完后再继续补已发送历史
</p>
</section>
<div v-if="error" class="feedback-banner error">
<div class="error-text">{{ error }}</div>
</div>
<section class="data-card">
<div class="data-card__toolbar">
<div class="toolbar-meta">
<h3 class="section-title">待发布与已发送记录</h3>
<p class="section-desc">分页与总数以服务端返回的 PublishTasks 结果为准</p>
<section class="content-section">
<div class="hero-card" style="padding-bottom: 0;">
<div class="hero-header table-toolbar">
<div class="toolbar-meta">
<h3 class="section-title">待发布与已发送记录</h3>
<p class="section-desc">待发布任务始终排在最前每页 10 分页与总数以服务端返回的 PublishTasks 为准</p>
</div>
<div class="toolbar-filters">
<a-input
v-model:value="searchTitle"
allow-clear
placeholder="搜索文章标题"
class="search-input"
@pressEnter="applySearch"
>
<template #prefix>
<SearchOutlined style="color: #94a3b8" />
</template>
</a-input>
<a-button type="primary" class="modern-btn" @click="applySearch">搜索</a-button>
</div>
</div>
<div class="toolbar-filters">
<a-input
v-model:value="searchTitle"
allow-clear
placeholder="搜索文章标题"
class="search-input"
@pressEnter="applySearch"
>
<template #prefix>
<SearchOutlined style="color: #94a3b8" />
</template>
</a-input>
<a-button type="primary" class="primary-btn" @click="applySearch">搜索</a-button>
</div>
</div>
<a-table
row-key="id"
class="publish-table"
class="modern-table publish-table"
:columns="tableColumns"
:data-source="publishTasks"
:pagination="false"
@@ -631,6 +651,7 @@ const tableScroll = { x: 1000 } as const;
@change="handlePageChange"
/>
</div>
</div>
</section>
</section>
</template>
@@ -639,135 +660,97 @@ const tableScroll = { x: 1000 } as const;
.page-container {
display: flex;
flex-direction: column;
gap: 16px;
gap: 32px;
padding-bottom: 40px;
max-width: 1400px;
}
/* Header Bar */
.page-header {
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
/* Hero Section (aligned with AccountsView) */
.hero-card {
background: #ffffff;
border-radius: 20px;
border: 1px solid #eef2f6;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px -2px rgba(0, 0, 0, 0.01);
overflow: hidden;
}
.page-header__title {
.hero-header {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
justify-content: space-between;
align-items: flex-start;
padding: 36px 40px;
}
.eyebrow {
margin: 0;
color: #94a3b8;
font-size: 11px;
margin: 0 0 12px;
color: #64748b;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.12em;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.page-header__title h2 {
.hero-title h2 {
margin: 0;
font-size: 22px;
font-weight: 700;
font-size: 32px;
font-weight: 800;
color: #0f172a;
letter-spacing: -0.01em;
line-height: 1.2;
letter-spacing: -0.02em;
}
.page-header__metrics {
.modern-btn {
border-radius: 8px;
height: 40px;
padding: 0 20px;
font-weight: 600;
}
/* Stats Strip */
.stats-strip {
display: flex;
gap: 8px;
margin-left: auto;
flex-wrap: wrap;
padding: 24px 40px;
background: #f8fafc;
border-top: 1px solid #eef2f6;
gap: 40px;
}
.metric {
.stat-card {
display: flex;
flex-direction: column;
gap: 2px;
align-items: flex-start;
min-width: 76px;
padding: 8px 14px;
border-radius: 10px;
background: #ffffff;
border: 1px solid #e6edf5;
gap: 8px;
flex: 1;
}
.metric--accent {
border-color: #fcd34d;
background: #fffbeb;
}
.metric--total {
background: #f8fafc;
}
.metric__label {
color: #64748b;
font-size: 11px;
.stat-label {
font-size: 14px;
font-weight: 500;
color: #64748b;
}
.metric__value {
color: #0f172a;
font-size: 18px;
.stat-value {
font-size: 40px;
font-weight: 700;
line-height: 1.1;
color: #0f172a;
line-height: 1;
font-feature-settings: "tnum";
}
.metric--accent .metric__value {
.stat-value--accent {
color: #b45309;
}
.page-header__actions {
display: flex;
align-items: center;
}
.ghost-btn {
border-radius: 8px;
height: 36px;
padding: 0 14px;
font-weight: 500;
border-color: #e2e8f0;
color: #475569;
}
.ghost-btn:hover,
.ghost-btn:focus {
border-color: #cbd5e1;
color: #0f172a;
background: #f8fafc;
}
.page-summary {
margin: 0;
color: #64748b;
font-size: 13px;
line-height: 1.5;
}
/* Data Card */
.data-card {
background: #ffffff;
border-radius: 16px;
border: 1px solid #eef2f6;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03);
overflow: hidden;
/* Content Section / Table Toolbar */
.content-section {
display: flex;
flex-direction: column;
gap: 24px;
}
.data-card__toolbar {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
padding: 18px 20px;
.table-toolbar {
padding: 24px 32px 16px;
border-bottom: 1px solid #eef2f6;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
@@ -778,21 +761,21 @@ const tableScroll = { x: 1000 } as const;
.section-title {
margin: 0;
font-size: 15px;
font-size: 20px;
font-weight: 700;
color: #0f172a;
}
.section-desc {
margin: 4px 0 0;
margin: 6px 0 0;
color: #64748b;
font-size: 12px;
font-size: 13px;
line-height: 1.5;
}
.toolbar-filters {
display: flex;
gap: 8px;
gap: 12px;
align-items: center;
flex-shrink: 0;
}
@@ -802,30 +785,27 @@ const tableScroll = { x: 1000 } as const;
border-radius: 8px;
}
.primary-btn {
border-radius: 8px;
height: 32px;
padding: 0 16px;
font-weight: 500;
/* Modern Enterprise Table (aligned with AccountsView) */
:deep(.publish-table) {
padding: 0 32px 16px;
}
/* Table */
:deep(.publish-table .ant-table) {
background: transparent;
}
:deep(.publish-table .ant-table-thead > tr > th) {
background-color: #fafbfc !important;
background-color: transparent !important;
color: #64748b;
font-weight: 600;
font-size: 12px;
font-size: 13px;
border-bottom: 1px solid #e2e8f0;
padding: 10px 14px;
padding: 16px 12px;
white-space: nowrap;
}
:deep(.publish-table .ant-table-tbody > tr > td) {
padding: 12px 14px;
padding: 16px 12px;
border-bottom: 1px solid #f1f5f9;
color: #0f172a;
vertical-align: middle;
@@ -874,7 +854,7 @@ const tableScroll = { x: 1000 } as const;
.cell-title {
font-weight: 600;
color: #0f172a;
font-size: 13px;
font-size: 14px;
line-height: 1.4;
display: block;
overflow: hidden;
@@ -885,14 +865,15 @@ const tableScroll = { x: 1000 } as const;
.cell-title--mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-weight: 500;
font-size: 12.5px;
font-size: 13px;
}
.cell-sub {
color: #64748b;
font-size: 12px;
font-size: 13px;
line-height: 1.5;
display: block;
margin-top: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -1055,8 +1036,7 @@ const tableScroll = { x: 1000 } as const;
display: flex;
align-items: center;
justify-content: flex-end;
padding: 12px 20px;
border-top: 1px solid #eef2f6;
padding: 16px 32px 24px;
background: transparent;
}