feat(client): show digital employee sync errors
This commit is contained in:
@@ -1189,6 +1189,24 @@
|
||||
border: 1px solid rgba(79,172,254,0.18); color: #16324e;
|
||||
background: rgba(247,251,255,0.96);
|
||||
}
|
||||
.de-management-sync-strip {
|
||||
display: flex; align-items: center; gap: 8px; min-height: 34px;
|
||||
margin: -2px 0 10px; padding: 7px 10px; border-radius: 12px;
|
||||
font-size: 12px; line-height: 1.35; color: #315576;
|
||||
border: 1px solid rgba(79,172,254,0.14); background: rgba(79,172,254,0.07);
|
||||
}
|
||||
.de-management-sync-strip > span:last-child {
|
||||
min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.de-management-sync-strip--success {
|
||||
color: #176a3a; border-color: rgba(27,156,90,0.2); background: rgba(27,156,90,0.08);
|
||||
}
|
||||
.de-management-sync-strip--warning {
|
||||
color: #7a5509; border-color: rgba(245,173,45,0.22); background: rgba(245,173,45,0.1);
|
||||
}
|
||||
.de-management-sync-strip--danger {
|
||||
color: #ad3434; border-color: rgba(224,72,72,0.2); background: rgba(224,72,72,0.08);
|
||||
}
|
||||
.de-duty-card { min-height: 120px; }
|
||||
.de-duty-card--selected {
|
||||
border-color: rgba(32,191,107,0.42);
|
||||
|
||||
@@ -52,6 +52,17 @@ interface DigitalPlanItem {
|
||||
source: 'plan_template';
|
||||
}
|
||||
|
||||
interface ManagementSyncStatus {
|
||||
running: boolean;
|
||||
syncing: boolean;
|
||||
enabled: boolean;
|
||||
endpoint: string | null;
|
||||
pending: number;
|
||||
failed: number;
|
||||
lastSyncAt: string | null;
|
||||
lastSyncError: string | null;
|
||||
}
|
||||
|
||||
// 技术侧 PlanTemplate 是任务模板来源;用户侧只展示为“任务”,运行时 Task/PlanStep 展示为“步骤”。
|
||||
|
||||
interface EmployeeVisualStateOption {
|
||||
@@ -394,6 +405,59 @@ function formatBeijingDateTime(value?: string | null): string {
|
||||
return `${part('month')}/${part('day')} ${part('hour')}:${part('minute')}`;
|
||||
}
|
||||
|
||||
function summarizeSyncError(value?: string | null): string {
|
||||
const error = compactText(value);
|
||||
if (!error) return '';
|
||||
return error.length > 140 ? `${error.slice(0, 140)}...` : error;
|
||||
}
|
||||
|
||||
function syncStatusTone(status: ManagementSyncStatus | null): 'info' | 'success' | 'warning' | 'danger' {
|
||||
if (!status) return 'info';
|
||||
if (!status.enabled || !status.endpoint) return 'warning';
|
||||
if (status.failed > 0 || status.lastSyncError) return 'danger';
|
||||
if (status.syncing || status.pending > 0) return 'warning';
|
||||
return status.lastSyncAt ? 'success' : 'info';
|
||||
}
|
||||
|
||||
function syncStatusLabel(status: ManagementSyncStatus | null): string {
|
||||
if (!status) return '未连接';
|
||||
if (!status.enabled) return '已停用';
|
||||
if (!status.endpoint) return '未配置';
|
||||
if (status.syncing) return '同步中';
|
||||
if (status.failed > 0 || status.lastSyncError) return '同步异常';
|
||||
if (status.pending > 0) return '待同步';
|
||||
return status.lastSyncAt ? '已同步' : '等待数据';
|
||||
}
|
||||
|
||||
function syncStatusSummary(status: ManagementSyncStatus | null): string {
|
||||
if (!status) return '当前环境未连接 qimingclaw 同步桥。';
|
||||
if (!status.enabled) return '管理端同步已停用。';
|
||||
if (!status.endpoint) return '管理端同步地址未配置。';
|
||||
const error = summarizeSyncError(status.lastSyncError);
|
||||
if (status.failed > 0 || error) {
|
||||
return [
|
||||
status.failed > 0 ? `${status.failed} 条失败` : '',
|
||||
status.pending > 0 ? `${status.pending} 条待同步` : '',
|
||||
error ? `失败原因:${error}` : '',
|
||||
].filter(Boolean).join(',');
|
||||
}
|
||||
if (status.syncing) return `正在同步,待同步 ${status.pending} 条。`;
|
||||
if (status.pending > 0) return `仍有 ${status.pending} 条待同步记录。`;
|
||||
return status.lastSyncAt
|
||||
? `最近同步:${formatBeijingDateTime(status.lastSyncAt) || status.lastSyncAt}`
|
||||
: '等待本地 outbox 数据。';
|
||||
}
|
||||
|
||||
async function readManagementSyncStatus(): Promise<ManagementSyncStatus | null> {
|
||||
const bridge = window.QimingClawBridge?.digital;
|
||||
if (!bridge?.getSyncStatus) return null;
|
||||
try {
|
||||
return await bridge.getSyncStatus();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function eventDisplayTime(event: DigitalEmployeeWorkEvent): string {
|
||||
const beijingTime = formatBeijingDateTime(event.updated_at);
|
||||
if (beijingTime) {
|
||||
@@ -569,6 +633,7 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
||||
const [reportScheduleTime, setReportScheduleTime] = useState('18:00');
|
||||
const [guideOpen, setGuideOpen] = useState(false);
|
||||
const [guideDismissedDate, setGuideDismissedDate] = useState<string | null>(() => readGuideDismissedDate());
|
||||
const [managementSyncStatus, setManagementSyncStatus] = useState<ManagementSyncStatus | null>(null);
|
||||
const mountedRef = useRef(false);
|
||||
const selectionDirtyRef = useRef(false);
|
||||
|
||||
@@ -623,7 +688,13 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
||||
setPlanDataError(error instanceof Error ? error.message : '任务加载失败');
|
||||
});
|
||||
|
||||
await Promise.allSettled([projectionPromise, jobsPromise]);
|
||||
const syncStatusPromise = readManagementSyncStatus()
|
||||
.then((status) => {
|
||||
if (!mountedRef.current) return;
|
||||
setManagementSyncStatus(status);
|
||||
});
|
||||
|
||||
await Promise.allSettled([projectionPromise, jobsPromise, syncStatusPromise]);
|
||||
if (mountedRef.current) {
|
||||
setProjectionLoading(false);
|
||||
}
|
||||
@@ -749,6 +820,10 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
||||
const isDemoProjection = workdayProjection?.source === 'demo';
|
||||
const projectionSourceLabel = isDemoProjection ? '演示兜底' : '真实数据';
|
||||
const projectionSourceClass = isDemoProjection ? 'de-badge-warning' : 'de-badge-success';
|
||||
const managementSyncTone = syncStatusTone(managementSyncStatus);
|
||||
const managementSyncLabel = syncStatusLabel(managementSyncStatus);
|
||||
const managementSyncSummary = syncStatusSummary(managementSyncStatus);
|
||||
const managementSyncError = summarizeSyncError(managementSyncStatus?.lastSyncError);
|
||||
const resolvedVisualState = resolveEmployeeVisualState({
|
||||
confirmed: Boolean(workday?.confirmed),
|
||||
currentEvent,
|
||||
@@ -863,6 +938,7 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
||||
}
|
||||
const syncStatus = await bridge.flushSync();
|
||||
if (!mountedRef.current) return;
|
||||
setManagementSyncStatus(syncStatus);
|
||||
const latestProjection = await getDigitalEmployeeWorkday(selectedDate);
|
||||
if (!mountedRef.current) return;
|
||||
setWorkdayProjection(latestProjection);
|
||||
@@ -1251,6 +1327,11 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={`de-management-sync-strip de-management-sync-strip--${managementSyncTone}`} title={managementSyncError || managementSyncSummary}>
|
||||
<span className={`de-badge ${badgeClass(managementSyncTone)}`}>{managementSyncLabel}</span>
|
||||
<span>{managementSyncSummary}</span>
|
||||
</div>
|
||||
|
||||
{(actionNotice || actionError || planDataError || workdayProjection?.demo_reason) && (
|
||||
<div className={`de-schedule-result ${actionError ? 'de-schedule-result--error' : 'de-schedule-result--success'}`} style={{ marginBottom: 10 }}>
|
||||
{actionError || actionNotice || planDataError || workdayProjection?.demo_reason}
|
||||
|
||||
Reference in New Issue
Block a user