fix: show sync failures on hover and preserve browser error details

This commit is contained in:
2026-10-05 22:14:15 +08:00
parent a899ea0caa
commit fa558d9a64
4 changed files with 104 additions and 7 deletions
@@ -1,7 +1,7 @@
// 监控账号列表:表格呈现账号画像、作品数据与同步状态。
import { useCallback, useEffect, useState } from 'react';
import { history } from '@umijs/max';
import { Alert, App, Avatar, Button, Card, Flex, Form, Modal, Popconfirm, Select, Space, Table, Tag, Typography } from 'antd';
import { Alert, App, Avatar, Button, Card, Flex, Form, Modal, Popconfirm, Select, Space, Table, Tag, Tooltip, Typography } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { CloudUploadOutlined, ReloadOutlined } from '@ant-design/icons';
import { getList, remove, creatorAction, creatorUpdate } from '@/services/api';
@@ -165,11 +165,14 @@ export default function MonitoringAccountList() {
const meta = syncStatusMeta[account.sync_status ?? ''] || { label: account.sync_status || '—' };
return (
<Flex vertical>
<div><Tag color={meta.color}>{meta.label}</Tag></div>
<div>
<Tooltip title={account.sync_status === 'failed' || account.sync_status === 'blocked' ? account.sync_error : undefined}>
<Tag color={meta.color}>{meta.label}</Tag>
</Tooltip>
</div>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
上次同步 {account.last_sync_at ? dateTime(account.last_sync_at) : '—'}
</Typography.Text>
{account.sync_error ? <Typography.Text type="danger" style={{ fontSize: 12 }}>{account.sync_error}</Typography.Text> : null}
</Flex>
);
},
@@ -12,6 +12,68 @@ const source = ts.createSourceFile(
);
const code = source.getFullText();
function syncRenderer() {
let meta;
let renderer;
function visit(node) {
if (ts.isVariableDeclaration(node) && node.name.getText(source) === 'syncStatusMeta') {
meta = node.initializer.getText(source);
}
if (ts.isObjectLiteralExpression(node) && node.properties.some(
(property) => ts.isPropertyAssignment(property)
&& property.name.getText(source) === 'title'
&& property.initializer.getText(source) === "'同步状态'",
)) {
renderer = node.properties.find((property) => property.name?.getText(source) === 'render').initializer.getText(source);
}
ts.forEachChild(node, visit);
}
visit(source);
assert.ok(meta && renderer, '同步列和状态定义必须存在');
const { outputText } = ts.transpileModule(`const syncStatusMeta = ${meta}; return (${renderer});`, {
compilerOptions: { jsx: ts.JsxEmit.React, target: ts.ScriptTarget.ES2022 },
});
return new Function('React', 'Flex', 'Tooltip', 'Tag', 'Typography', 'dateTime', outputText)(
require('react'), 'Flex', 'Tooltip', 'Tag', { Text: 'Text' }, (value) => value || '—',
);
}
function elements(node) {
return node && typeof node === 'object'
? [node, ...[node.props?.children].flat(Infinity).flatMap(elements)]
: [];
}
test('失败原因仅在悬停同步失败标签时显示,完整保留长错误', () => {
const render = syncRenderer();
const reason = 'douyin browser request rejected with HTTP 502: ' + '执行页面请求超时'.repeat(80);
for (const status of ['failed', 'blocked']) {
const nodes = elements(render(null, { sync_status: status, sync_error: reason, last_sync_at: '2026-05-14' }));
const tooltip = nodes.find((node) => node.type === 'Tooltip');
assert.ok(tooltip, '失败原因必须使用 Tooltip');
assert.equal(tooltip.props.title, reason);
assert.equal(tooltip.props.children.type, 'Tag', '浮层仅绑定状态标签');
assert.ok(nodes.every((node) => node.props?.children !== reason), '错误不占用表格行空间');
}
});
test('其他同步状态及无失败原因时不显示错误浮层', () => {
const render = syncRenderer();
for (const status of ['idle', 'running', 'paused', 'failed', 'blocked', 'unknown', undefined]) {
for (const sync_error of ['', null, undefined]) {
const nodes = elements(render(null, { sync_status: status, sync_error }));
const tag = nodes.find((node) => node.type === 'Tag');
assert.ok(tag);
const tooltip = nodes.find((node) => node.type === 'Tooltip');
assert.ok(!tooltip || !tooltip.props.title);
}
}
for (const status of ['idle', 'running', 'paused']) {
const nodes = elements(render(null, { sync_status: status, sync_error: '上次失败' }));
assert.ok(nodes.every((node) => node.type !== 'Tooltip' || !node.props.title));
}
});
test('监控账号列表使用 Table,一行一个账号,保留详情入口和操作', () => {
assert.match(code, /<Table\b/);
assert.match(code, /rowKey="id"/);