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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user