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; 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"] { function normalizePublishTaskStatus(status: DesktopTaskInfo["status"]): DesktopTaskInfo["status"] {
return status === "unknown" ? "failed" : 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, leaseExpiresAt: task.lease_expires_at ? parseTimestamp(task.lease_expires_at) : null,
createdAt: parseTimestamp(task.created_at), createdAt: parseTimestamp(task.created_at),
updatedAt: parseTimestamp(task.updated_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), externalManageUrl: extractString(result.external_manage_url),
errorMessage: normalizeTaskErrorMessage( errorMessage: normalizeTaskErrorMessage(
extractString(taskError.message) extractString(taskError.message)
@@ -403,66 +424,65 @@ const tableScroll = { x: 1000 } as const;
<template> <template>
<section class="page-container"> <section class="page-container">
<header class="page-header"> <section class="hero-card">
<div class="page-header__title"> <div class="hero-header">
<p class="eyebrow">Publish Execution Desk</p> <div class="hero-title">
<h2>发布管理</h2> <p class="eyebrow">PUBLISH EXECUTION DESK</p>
</div> <h2>发布管理</h2>
<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>
</div> </div>
<div class="metric"> <div class="hero-actions">
<span class="metric__label">已发送</span> <a-button type="primary" ghost class="modern-btn" :loading="loading" @click="refreshTasks()">
<span class="metric__value">{{ historyTotal }}</span> <template #icon><ReloadOutlined /></template>
</div> 刷新状态
<div class="metric metric--total"> </a-button>
<span class="metric__label">历史总数</span>
<span class="metric__value">{{ totalCount }}</span>
</div> </div>
</div> </div>
<div class="page-header__actions"> <div class="stats-strip">
<a-button class="ghost-btn" :loading="loading" @click="refreshTasks()"> <div class="stat-card">
<template #icon><ReloadOutlined /></template> <div class="stat-label">等待发布</div>
刷新 <div class="stat-value" :class="{ 'stat-value--accent': pendingCount > 0 }">{{ pendingCount }}</div>
</a-button> </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> </div>
</header> </section>
<p class="page-summary">
待发布任务始终排在最前每页 10 当前页 PublishTasks 展示完后再继续补已发送历史
</p>
<div v-if="error" class="feedback-banner error"> <div v-if="error" class="feedback-banner error">
<div class="error-text">{{ error }}</div> <div class="error-text">{{ error }}</div>
</div> </div>
<section class="data-card"> <section class="content-section">
<div class="data-card__toolbar"> <div class="hero-card" style="padding-bottom: 0;">
<div class="toolbar-meta"> <div class="hero-header table-toolbar">
<h3 class="section-title">待发布与已发送记录</h3> <div class="toolbar-meta">
<p class="section-desc">分页与总数以服务端返回的 PublishTasks 结果为准</p> <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>
<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 <a-table
row-key="id" row-key="id"
class="publish-table" class="modern-table publish-table"
:columns="tableColumns" :columns="tableColumns"
:data-source="publishTasks" :data-source="publishTasks"
:pagination="false" :pagination="false"
@@ -631,6 +651,7 @@ const tableScroll = { x: 1000 } as const;
@change="handlePageChange" @change="handlePageChange"
/> />
</div> </div>
</div>
</section> </section>
</section> </section>
</template> </template>
@@ -639,135 +660,97 @@ const tableScroll = { x: 1000 } as const;
.page-container { .page-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 32px;
padding-bottom: 40px; padding-bottom: 40px;
max-width: 1400px; max-width: 1400px;
} }
/* Header Bar */ /* Hero Section (aligned with AccountsView) */
.page-header { .hero-card {
display: flex; background: #ffffff;
align-items: center; border-radius: 20px;
gap: 20px; border: 1px solid #eef2f6;
flex-wrap: wrap; 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; display: flex;
flex-direction: column; justify-content: space-between;
gap: 2px; align-items: flex-start;
min-width: 0; padding: 36px 40px;
} }
.eyebrow { .eyebrow {
margin: 0; margin: 0 0 12px;
color: #94a3b8; color: #64748b;
font-size: 11px; font-size: 13px;
font-weight: 700; font-weight: 700;
letter-spacing: 0.12em; letter-spacing: 0.1em;
text-transform: uppercase; text-transform: uppercase;
} }
.page-header__title h2 { .hero-title h2 {
margin: 0; margin: 0;
font-size: 22px; font-size: 32px;
font-weight: 700; font-weight: 800;
color: #0f172a; color: #0f172a;
letter-spacing: -0.01em; letter-spacing: -0.02em;
line-height: 1.2;
} }
.page-header__metrics { .modern-btn {
border-radius: 8px;
height: 40px;
padding: 0 20px;
font-weight: 600;
}
/* Stats Strip */
.stats-strip {
display: flex; display: flex;
gap: 8px; padding: 24px 40px;
margin-left: auto; background: #f8fafc;
flex-wrap: wrap; border-top: 1px solid #eef2f6;
gap: 40px;
} }
.metric { .stat-card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2px; gap: 8px;
align-items: flex-start; flex: 1;
min-width: 76px;
padding: 8px 14px;
border-radius: 10px;
background: #ffffff;
border: 1px solid #e6edf5;
} }
.metric--accent { .stat-label {
border-color: #fcd34d; font-size: 14px;
background: #fffbeb;
}
.metric--total {
background: #f8fafc;
}
.metric__label {
color: #64748b;
font-size: 11px;
font-weight: 500; font-weight: 500;
color: #64748b;
} }
.metric__value { .stat-value {
color: #0f172a; font-size: 40px;
font-size: 18px;
font-weight: 700; font-weight: 700;
line-height: 1.1; color: #0f172a;
line-height: 1;
font-feature-settings: "tnum"; font-feature-settings: "tnum";
} }
.metric--accent .metric__value { .stat-value--accent {
color: #b45309; color: #b45309;
} }
.page-header__actions { /* Content Section / Table Toolbar */
display: flex; .content-section {
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;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 24px;
} }
.data-card__toolbar { .table-toolbar {
display: flex; padding: 24px 32px 16px;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
padding: 18px 20px;
border-bottom: 1px solid #eef2f6; border-bottom: 1px solid #eef2f6;
align-items: center;
gap: 16px;
flex-wrap: wrap; flex-wrap: wrap;
} }
@@ -778,21 +761,21 @@ const tableScroll = { x: 1000 } as const;
.section-title { .section-title {
margin: 0; margin: 0;
font-size: 15px; font-size: 20px;
font-weight: 700; font-weight: 700;
color: #0f172a; color: #0f172a;
} }
.section-desc { .section-desc {
margin: 4px 0 0; margin: 6px 0 0;
color: #64748b; color: #64748b;
font-size: 12px; font-size: 13px;
line-height: 1.5; line-height: 1.5;
} }
.toolbar-filters { .toolbar-filters {
display: flex; display: flex;
gap: 8px; gap: 12px;
align-items: center; align-items: center;
flex-shrink: 0; flex-shrink: 0;
} }
@@ -802,30 +785,27 @@ const tableScroll = { x: 1000 } as const;
border-radius: 8px; border-radius: 8px;
} }
.primary-btn { /* Modern Enterprise Table (aligned with AccountsView) */
border-radius: 8px; :deep(.publish-table) {
height: 32px; padding: 0 32px 16px;
padding: 0 16px;
font-weight: 500;
} }
/* Table */
:deep(.publish-table .ant-table) { :deep(.publish-table .ant-table) {
background: transparent; background: transparent;
} }
:deep(.publish-table .ant-table-thead > tr > th) { :deep(.publish-table .ant-table-thead > tr > th) {
background-color: #fafbfc !important; background-color: transparent !important;
color: #64748b; color: #64748b;
font-weight: 600; font-weight: 600;
font-size: 12px; font-size: 13px;
border-bottom: 1px solid #e2e8f0; border-bottom: 1px solid #e2e8f0;
padding: 10px 14px; padding: 16px 12px;
white-space: nowrap; white-space: nowrap;
} }
:deep(.publish-table .ant-table-tbody > tr > td) { :deep(.publish-table .ant-table-tbody > tr > td) {
padding: 12px 14px; padding: 16px 12px;
border-bottom: 1px solid #f1f5f9; border-bottom: 1px solid #f1f5f9;
color: #0f172a; color: #0f172a;
vertical-align: middle; vertical-align: middle;
@@ -874,7 +854,7 @@ const tableScroll = { x: 1000 } as const;
.cell-title { .cell-title {
font-weight: 600; font-weight: 600;
color: #0f172a; color: #0f172a;
font-size: 13px; font-size: 14px;
line-height: 1.4; line-height: 1.4;
display: block; display: block;
overflow: hidden; overflow: hidden;
@@ -885,14 +865,15 @@ const tableScroll = { x: 1000 } as const;
.cell-title--mono { .cell-title--mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-weight: 500; font-weight: 500;
font-size: 12.5px; font-size: 13px;
} }
.cell-sub { .cell-sub {
color: #64748b; color: #64748b;
font-size: 12px; font-size: 13px;
line-height: 1.5; line-height: 1.5;
display: block; display: block;
margin-top: 4px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@@ -1055,8 +1036,7 @@ const tableScroll = { x: 1000 } as const;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
padding: 12px 20px; padding: 16px 32px 24px;
border-top: 1px solid #eef2f6;
background: transparent; background: transparent;
} }