fix(web): 强制同步运行中禁用按钮——列表/详情统一 sync_status=running 判定

- 列表行与监控详情页:sync_status=running 时按钮显示「同步中…」并禁用,
  防止与调度器并发触发导致 409;自有账号详情请求期间 loading 禁用
- conflictMessage:409/503 无 hint 时也透出后端分类 error 文案,
  兜住「请求失败」空白提示;回归见 TestCompetitorSyncClaimFailureClassification
This commit is contained in:
2026-09-30 12:55:41 +08:00
parent c6a4d20c03
commit d535f178d6
4 changed files with 18 additions and 7 deletions
@@ -64,7 +64,7 @@ export default function MonitoringAccountList() {
load();
}, [load]);
// 强制同步:后端用临时匿名浏览器采集公开作品,无需自有账号身份。
// 强制同步:后端用临时匿名浏览器采集公开作品,无需自有账号身份;409/503 直接展示后端分类文案。
async function syncCompetitor(competitor: CompetitorRow) {
setActionBusy(competitor.id);
try {
@@ -187,7 +187,9 @@ export default function MonitoringAccountList() {
render: (_, account) => (
<Space wrap>
<Button type="text" size="small" onClick={() => openTags(account)}>编辑标签</Button>
<Button type="text" size="small" loading={actionBusy === account.id} disabled={actionBusy === account.id} onClick={() => syncCompetitor(account)}>强制同步</Button>
<Button type="text" size="small" loading={actionBusy === account.id} disabled={actionBusy === account.id || account.sync_status === 'running'} onClick={() => syncCompetitor(account)}>
{account.sync_status === 'running' ? '同步中…' : '强制同步'}
</Button>
<Button type="text" size="small" disabled={actionBusy === account.id} onClick={() => toggleCompetitor(account)}>
{account.enabled ? '暂停' : '恢复'}
</Button>
+1
View File
@@ -137,6 +137,7 @@ export default function Page() {
<Space>
<Button onClick={() => history.push('/accounts')}>返回社媒账号</Button>
<Button
loading={actionBusy}
disabled={actionBusy || !account}
onClick={async () => {
// 强制同步:后端复用账号自身运行环境采集,需已登录+环境就绪(否则 409/503 提示原因)。
@@ -65,15 +65,19 @@ export default function Page() {
}, [loadCompetitor]);
const syncMeta = syncStatusMeta[competitor?.sync_status ?? ''] || { label: competitor?.sync_status || '—' };
// 同步进行中(调度器采集或手动同步):按钮禁用防止重复触发,后端抢锁失败也会返回 409 分类提示兜底。
const syncRunning = competitor?.sync_status === 'running';
// 页头 actions:返回/同步/暂停恢复,对齐账号详情页形态。
usePageActions(
<Space>
<Button onClick={() => history.push('/accounts/monitoring')}>返回监控账号</Button>
<Button
disabled={actionBusy || !competitor}
loading={actionBusy}
disabled={actionBusy || syncRunning || !competitor}
onClick={async () => {
// 强制同步:后端用临时匿名浏览器采集公开作品,无需自有账号身份。
// 强制同步:后端用临时匿名浏览器采集公开作品,无需自有账号身份;
// 409/503 直接展示后端分类文案(同步中/已停用/网关不可用)。
if (!competitor) return;
setActionBusy(true);
try {
@@ -87,7 +91,7 @@ export default function Page() {
}
}}
>
强制同步
{syncRunning ? '同步中…' : '强制同步'}
</Button>
<Button
type="primary"
@@ -111,7 +115,7 @@ export default function Page() {
{competitor ? (competitor.enabled ? '暂停监控' : '恢复监控') : '暂停监控'}
</Button>
</Space>,
[competitor?.id, competitor?.enabled],
[competitor?.id, competitor?.enabled, competitor?.sync_status],
);
if (pending) return <Card loading />;
+5 -1
View File
@@ -5,11 +5,15 @@ export function dateTime(value?: string | number | null): string {
return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString();
}
// 对齐归档版 conflictMessage:优先后端 error 字段,409 冲突时附加提示。
// 对齐归档版 conflictMessage:优先后端 error 字段;409/503 冲突类错误即使无 hint 也直接透出后端
// 分类文案(如「该竞品正在同步中」),避免接口报错时界面无任何反馈。
export function conflictMessage(error: any, conflictHint?: string): string {
const status = error?.status ?? error?.body?.status;
const message = error?.message || error?.body?.error || '请求失败';
if (status === 409 && conflictHint) return `${message}:${conflictHint}`;
if ((status === 409 || status === 503) && message === '请求失败') {
return conflictHint ? conflictHint : `请求失败(${status}),请稍后重试`;
}
return message;
}