merge: integrate display-only dashboard metrics

This commit is contained in:
inman committed 2026-09-02 15:16:26 +08:00
commit cc09506a06
6 files changed
+24 -45

No files matched your search

@@ -10,12 +10,21 @@ const [app, index, styles, taskService, server] = await Promise.all([
readFile(new URL('../control-plane/src/server.ts', import.meta.url), 'utf8') readFile(new URL('../control-plane/src/server.ts', import.meta.url), 'utf8')
]); ]);
test('dashboard summary cards share one visual treatment and expose an accessible selected state', () => { test('dashboard summary cards are uniform display-only totals and leave filtering to the explicit controls', () => {
const summaryRenderer = app.slice(
app.indexOf('function renderOperationsDashboardSummary()'),
app.indexOf('function operationsDashboardRankScale')
);
assert.match(app, /label: '操作次数'[^\n]+tone: 'total'/); assert.match(app, /label: '操作次数'[^\n]+tone: 'total'/);
assert.doesNotMatch(app, /tone: 'primary'/); assert.doesNotMatch(app, /tone: 'primary'/);
assert.match(app, /card\.setAttribute\('aria-pressed', String\(isSelected\)\)/); assert.match(summaryRenderer, /const card = el\('article', `operations-dashboard-stat operations-dashboard-stat-\$\{item\.tone\}`\)/);
assert.doesNotMatch(summaryRenderer, /selectedStatus|isSelected|operationsStatus|aria-pressed/);
assert.doesNotMatch(app, /\$\('#operationsDashboardSummary'\)\?\.addEventListener\('click'/);
assert.match(index, /<option value="all">全部结果<\/option>/);
assert.match(app, /function resetOperationsDashboardFilters\(\)[\s\S]*?\$\('#operationsDashboardStatus'\)\.value = 'all';/);
assert.match(styles, /\.operations-dashboard-summary\s*{[\s\S]*?grid-template-columns: repeat\(5, minmax\(0, 1fr\)\)/); assert.match(styles, /\.operations-dashboard-summary\s*{[\s\S]*?grid-template-columns: repeat\(5, minmax\(0, 1fr\)\)/);
assert.match(styles, /\.operations-dashboard-stat\.is-active\s*{[\s\S]*?background: var\(--dashboard-accent-soft\);[\s\S]*?transform: none;/); assert.doesNotMatch(styles, /\.operations-dashboard-stat\[data-operations-status\]/);
assert.doesNotMatch(styles, /\.operations-dashboard-stat\.is-active/);
assert.doesNotMatch(styles, /operations-dashboard-stat-primary/); assert.doesNotMatch(styles, /operations-dashboard-stat-primary/);
}); });
+6 -20
View File
@@ -1453,22 +1453,14 @@ function renderOperationsDashboardSummary() {
container.replaceChildren(); container.replaceChildren();
const summary = operationsDashboardData?.summary || {}; const summary = operationsDashboardData?.summary || {};
const cards = [ const cards = [
{ label: '操作次数', value: summary.total || 0, hint: '全部任务操作', status: 'all', tone: 'total' }, { label: '操作次数', value: summary.total || 0, hint: '全部任务操作', tone: 'total' },
{ label: '成功次数', value: summary.completed || 0, hint: '已成功完成', status: 'completed', tone: 'success' }, { label: '成功次数', value: summary.completed || 0, hint: '已成功完成', tone: 'success' },
{ label: '进行中', value: Number(summary.active || 0) + Number(summary.attention || 0), hint: '正在处理', status: 'active', tone: 'active' }, { label: '进行中', value: Number(summary.active || 0) + Number(summary.attention || 0), hint: '正在处理', tone: 'active' },
{ label: '未成功', value: summary.failed || 0, hint: '需要继续处理', status: 'failed', tone: 'failed' }, { label: '未成功', value: summary.failed || 0, hint: '需要继续处理', tone: 'failed' },
{ label: '操作员工', value: summary.user_count || 0, hint: '参与操作人数', status: '', tone: 'people' } { label: '操作员工', value: summary.user_count || 0, hint: '参与操作人数', tone: 'people' }
]; ];
const selectedStatus = String($('#operationsDashboardStatus')?.value || 'all');
for (const item of cards) { for (const item of cards) {
const isSelected = Boolean(item.status && selectedStatus === item.status); const card = el('article', `operations-dashboard-stat operations-dashboard-stat-${item.tone}`);
const card = el(item.status ? 'button' : 'article', `operations-dashboard-stat operations-dashboard-stat-${item.tone}${isSelected ? ' is-active' : ''}`);
if (item.status) {
card.type = 'button';
card.dataset.operationsStatus = item.status;
card.setAttribute('aria-label', `按${item.label}筛选任务`);
card.setAttribute('aria-pressed', String(isSelected));
}
card.append(el('span', '', item.label)); card.append(el('span', '', item.label));
card.append(el('strong', '', String(item.value))); card.append(el('strong', '', String(item.value)));
card.append(el('small', '', item.hint)); card.append(el('small', '', item.hint));
@@ -5742,12 +5734,6 @@ document.addEventListener('DOMContentLoaded', async () => {
$('#operationsDashboardActor').value = button.dataset.operationsActorId || ''; $('#operationsDashboardActor').value = button.dataset.operationsActorId || '';
refreshOperationsDashboardAfterDrilldown(); refreshOperationsDashboardAfterDrilldown();
}); });
$('#operationsDashboardSummary')?.addEventListener('click', (event) => {
const button = event.target.closest('[data-operations-status]');
if (!button) return;
$('#operationsDashboardStatus').value = button.dataset.operationsStatus || 'all';
refreshOperationsDashboardAfterDrilldown();
});
$('#operationsDashboardBusinesses')?.addEventListener('click', (event) => { $('#operationsDashboardBusinesses')?.addEventListener('click', (event) => {
const button = event.target.closest('[data-operations-business]'); const button = event.target.closest('[data-operations-business]');
if (!button?.dataset.operationsBusiness) return; if (!button?.dataset.operationsBusiness) return;
+2 -2
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#edf2f4"> <meta name="theme-color" content="#edf2f4">
<title>AI操作台 · LianSyn-platform</title> <title>AI操作台 · LianSyn-platform</title>
<link rel="stylesheet" href="styles.css?v=20260902-dashboard-mobile-share-1"> <link rel="stylesheet" href="styles.css?v=20260902-agentbus-dashboard-metrics-filter-1">
</head> </head>
<body> <body>
<main class="app-shell"> <main class="app-shell">
@@ -407,6 +407,6 @@
</section> </section>
</main> </main>
<script src="app.js?v=20260902-agentbus-dashboard-filter-1"></script> <script src="app.js?v=20260902-agentbus-dashboard-metrics-filter-1"></script>
</body> </body>
</html> </html>
-17
View File
@@ -3244,23 +3244,6 @@ textarea {
text-align: left; text-align: left;
} }
.operations-dashboard-stat[data-operations-status] {
cursor: pointer;
}
.operations-dashboard-stat[data-operations-status]:not(.is-active):hover {
border-color: #8daf9f;
box-shadow: 0 9px 24px rgba(32, 79, 67, 0.09);
transform: translateY(-2px);
}
.operations-dashboard-stat.is-active {
border-color: var(--dashboard-accent);
background: var(--dashboard-accent-soft);
box-shadow: inset 0 0 0 1px var(--dashboard-accent), 0 9px 24px rgba(32, 79, 67, 0.09);
transform: none;
}
.operations-dashboard-stat > span { .operations-dashboard-stat > span {
color: var(--dashboard-muted); color: var(--dashboard-muted);
font-size: 12px; font-size: 12px;
@@ -290,7 +290,8 @@ test('operator UI exposes role-aware accounts, executive drill-through, original
assert.match(app, /\/api\/operations-dashboard\?/); assert.match(app, /\/api\/operations-dashboard\?/);
assert.match(app, /\/api\/operations-dashboard\/tasks\/\$\{encodeURIComponent\(taskId\)\}/); assert.match(app, /\/api\/operations-dashboard\/tasks\/\$\{encodeURIComponent\(taskId\)\}/);
assert.match(app, /business_route_id/); assert.match(app, /business_route_id/);
assert.match(app, /data-operations-status|dataset\.operationsStatus/); assert.doesNotMatch(app, /data-operations-status|dataset\.operationsStatus/);
assert.match(app, /\$\('#operationsDashboardStatus'\)\.value = 'all'/);
assert.match(app, /data-operations-business|dataset\.operationsBusiness/); assert.match(app, /data-operations-business|dataset\.operationsBusiness/);
assert.match(app, /function operationsDashboardRankScale/); assert.match(app, /function operationsDashboardRankScale/);
assert.match(app, /operationsDashboardRankBar/); assert.match(app, /operationsDashboardRankBar/);
+2 -2
View File
@@ -1231,8 +1231,8 @@ test('operator page has a login gate and uses the durable task API', async () =>
const inpage = await readFile(new URL('../../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8'); const inpage = await readFile(new URL('../../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8');
assert.match(index, /id="loginPanel"/); assert.match(index, /id="loginPanel"/);
assert.match(index, /id="workbench"[^>]*hidden/); assert.match(index, /id="workbench"[^>]*hidden/);
assert.match(index, /styles\.css\?v=20260902-dashboard-mobile-share-1/); assert.match(index, /styles\.css\?v=20260902-agentbus-dashboard-metrics-filter-1/);
assert.match(index, /app\.js\?v=20260902-agentbus-dashboard-filter-1/); assert.match(index, /app\.js\?v=20260902-agentbus-dashboard-metrics-filter-1/);
assert.match(index, /id="statusDetailsPopover"/); assert.match(index, /id="statusDetailsPopover"/);
assert.match(index, /id="statusDetailsRefresh"/); assert.match(index, /id="statusDetailsRefresh"/);
assert.match(app, /apiRequest\(`\/api\/tasks\?\$\{params\.toString\(\)\}`/); assert.match(app, /apiRequest\(`\/api\/tasks\?\$\{params\.toString\(\)\}`/);