feat: compact leadership dashboard layout

This commit is contained in:
inman
2026-09-02 10:58:07 +08:00
parent 3ed3af1feb
commit f11aa6416b
6 changed files with 625 additions and 290 deletions

View File

@@ -1220,9 +1220,9 @@ function operationsDashboardStatusAppearance(outcomeKind) {
}
function operationsDashboardOutcomeLabel(outcomeKind, fallback = '') {
if (outcomeKind === 'completed') return '已完成';
if (outcomeKind === 'completed') return '成';
if (outcomeKind === 'attention') return '待跟进';
if (outcomeKind === 'failed') return '未成';
if (outcomeKind === 'failed') return '未成';
if (outcomeKind === 'cancelled') return '已取消';
if (outcomeKind === 'active') return '进行中';
return String(fallback || '进行中');
@@ -1258,17 +1258,6 @@ function operationsDashboardSafeError(error, fallback = '看板暂时无法更
return fallback;
}
function operationsDashboardCompletionRate(completed, total) {
if (!Number(total)) return 0;
return Math.round((Number(completed || 0) / Number(total)) * 1_000) / 10;
}
function operationsDashboardShortDay(value) {
const date = new Date(`${value}T00:00:00+08:00`);
if (Number.isNaN(date.getTime())) return value;
return new Intl.DateTimeFormat('zh-CN', { month: 'numeric', day: 'numeric' }).format(date);
}
function operationsDashboardBusinessLabel(value) {
const label = String(value || '').trim();
return !label || label === '未识别业务' ? '其他任务' : label;
@@ -1286,7 +1275,7 @@ function renderOperationsDashboardActorOptions(actors) {
const select = $('#operationsDashboardActor');
if (!select) return;
const selected = select.value;
select.replaceChildren(new Option('全部员', ''));
select.replaceChildren(new Option('全部员', ''));
for (const actor of actors || []) {
const state = actor.is_active ? '' : ' · 已停用';
select.append(new Option(`${actor.username} · ${accountRoleLabel(actor.role)}${state}`, actor.id));
@@ -1305,36 +1294,18 @@ function renderOperationsDashboardBusinessOptions(options) {
if ([...select.options].some((option) => option.value === selected)) select.value = selected;
}
function renderOperationsDashboardActiveFilters() {
const container = $('#operationsDashboardActiveFilters');
if (!container) return;
container.replaceChildren();
const actor = $('#operationsDashboardActor')?.selectedOptions?.[0]?.textContent || '全部人员';
const business = $('#operationsDashboardBusiness')?.selectedOptions?.[0]?.textContent || '全部类型';
const status = $('#operationsDashboardStatus')?.selectedOptions?.[0]?.textContent || '全部进展';
const from = String($('#operationsDashboardFrom')?.value || '');
const to = String($('#operationsDashboardTo')?.value || '');
const search = String($('#operationsDashboardSearch')?.value || '').trim();
container.append(el('span', 'operations-dashboard-filter-label', '当前查看'));
for (const value of [`${from.replaceAll('-', '/')}${to.replaceAll('-', '/')}`, actor, business, status]) {
container.append(el('span', 'operations-dashboard-filter-chip', value));
}
if (search) container.append(el('span', 'operations-dashboard-filter-chip', `包含“${search}`));
}
function renderOperationsDashboardSummary() {
const container = $('#operationsDashboardSummary');
if (!container) return;
container.replaceChildren();
const summary = operationsDashboardData?.summary || {};
const completionRate = operationsDashboardCompletionRate(summary.completed, summary.total);
const cards = [
{ label: '任务总量', value: summary.total || 0, hint: '点击查看全部工作', status: 'all', tone: 'primary' },
{ label: '已完成', value: summary.completed || 0, hint: `完成率 ${completionRate}%`, status: 'completed', tone: 'success' },
{ label: '进行中', value: summary.active || 0, hint: '正在推进的工作', status: 'active', tone: 'active' },
{ label: '操作次数', value: summary.total || 0, hint: '全部任务操作', status: 'all', tone: 'primary' },
{ label: '成功次数', value: summary.completed || 0, hint: '已成功完成', status: 'completed', tone: 'success' },
{ label: '进行中', value: summary.active || 0, hint: '正在处理', status: 'active', tone: 'active' },
{ label: '待跟进', value: summary.attention || 0, hint: '等待补充或确认', status: 'attention', tone: 'attention' },
{ label: '未成', value: summary.failed || 0, hint: '需要检查和复盘', status: 'failed', tone: 'failed' },
{ label: '参与人员', value: summary.user_count || 0, hint: '下方可查看个人表现', status: '', tone: 'people' }
{ label: '未成', value: summary.failed || 0, hint: '需要继续处理', status: 'failed', tone: 'failed' },
{ label: '操作员工', value: summary.user_count || 0, hint: '参与操作人数', status: '', tone: 'people' }
];
const selectedStatus = String($('#operationsDashboardStatus')?.value || 'all');
for (const item of cards) {
@@ -1342,7 +1313,7 @@ function renderOperationsDashboardSummary() {
if (item.status) {
card.type = 'button';
card.dataset.operationsStatus = item.status;
card.setAttribute('aria-label', `查看${item.label}任务`);
card.setAttribute('aria-label', `${item.label}筛选任务`);
}
card.append(el('span', '', item.label));
card.append(el('strong', '', String(item.value)));
@@ -1351,40 +1322,6 @@ function renderOperationsDashboardSummary() {
}
}
function renderOperationsDashboardDays() {
const container = $('#operationsDashboardDays');
if (!container) return;
container.replaceChildren();
const days = [...(operationsDashboardData?.days || [])].sort((left, right) => left.key.localeCompare(right.key));
const todayKey = operationsDashboardDateValue(new Date());
const today = days.find((item) => item.key === todayKey);
$('#operationsDashboardTodayCount').textContent = `今天 ${today?.total || 0}`;
if (!days.length) {
container.append(el('p', 'operations-dashboard-empty', '当前范围内还没有任务数据。'));
return;
}
const visibleDays = days.slice(-31);
const maxTotal = Math.max(...visibleDays.map((item) => Number(item.total || 0)), 1);
for (const item of visibleDays) {
const button = el('button', 'operations-dashboard-day-column');
button.type = 'button';
button.dataset.operationsDay = item.key;
button.setAttribute('aria-label', `${item.label}${item.total} 项任务,完成 ${item.completed}`);
const bars = el('span', 'operations-dashboard-day-bars');
const completedBar = el('i', 'is-completed');
const remainingBar = el('i', 'is-remaining');
completedBar.style.height = `${Math.max(0, (Number(item.completed || 0) / maxTotal) * 100)}%`;
remainingBar.style.height = `${Math.max(0, ((Number(item.total || 0) - Number(item.completed || 0)) / maxTotal) * 100)}%`;
bars.append(remainingBar, completedBar);
button.append(
el('strong', '', String(item.total)),
bars,
el('span', '', operationsDashboardShortDay(item.key))
);
container.append(button);
}
}
function renderOperationsDashboardBusinesses() {
const container = $('#operationsDashboardBusinesses');
if (!container) return;
@@ -1394,25 +1331,26 @@ function renderOperationsDashboardBusinesses() {
container.append(el('p', 'operations-dashboard-empty', '当前范围内还没有任务类型数据。'));
return;
}
const maxTotal = Math.max(...businesses.map((item) => Number(item.total || 0)), 1);
for (const item of businesses) {
for (const [index, item] of businesses.entries()) {
const button = el('button', 'operations-dashboard-business-row');
button.type = 'button';
button.dataset.operationsBusiness = item.key;
const heading = el('span', 'operations-dashboard-business-heading');
heading.append(el('strong', '', operationsDashboardBusinessLabel(item.label)), el('b', '', String(item.total)));
const track = el('span', 'operations-dashboard-business-track');
const fill = el('i', '');
fill.style.width = `${Math.max(4, (Number(item.total || 0) / maxTotal) * 100)}%`;
track.append(fill);
const rate = operationsDashboardCompletionRate(item.completed, item.total);
button.append(heading, track, el('small', '', `已完成 ${item.completed} 项 · 完成率 ${rate}%`));
button.setAttribute('aria-label', `${operationsDashboardBusinessLabel(item.label)},操作 ${item.total} 次,成功 ${item.completed}`);
button.append(
el('span', 'operations-dashboard-rank-number', String(index + 1).padStart(2, '0')),
el('strong', 'operations-dashboard-rank-name', operationsDashboardBusinessLabel(item.label))
);
const metrics = el('span', 'operations-dashboard-rank-metrics');
metrics.append(
el('span', '', `操作 ${Number(item.total || 0)}`),
el('span', 'is-success', `成功 ${Number(item.completed || 0)}`)
);
button.append(metrics);
container.append(button);
}
}
function renderOperationsDashboardBreakdowns() {
renderOperationsDashboardDays();
renderOperationsDashboardBusinesses();
}
@@ -1421,16 +1359,17 @@ function renderOperationsDashboardUsers() {
if (!container) return;
container.replaceChildren();
const users = operationsDashboardData?.users || [];
$('#operationsDashboardUserCount').textContent = `${users.length} 人参与`;
$('#operationsDashboardUserCount').textContent = `${users.length} 名员工`;
if (!users.length) {
container.append(el('p', 'operations-dashboard-empty', '当前范围内还没有人员工作数据。'));
return;
}
const maxTotal = Math.max(...users.map((item) => Number(item.total || 0)), 1);
for (const item of users) {
for (const [index, item] of users.entries()) {
const button = el('button', 'operations-dashboard-user-row');
button.type = 'button';
button.dataset.operationsActorId = item.actor.id;
button.setAttribute('aria-label', `${item.actor.username},操作 ${item.total} 次,成功 ${item.completed}`);
button.append(el('span', 'operations-dashboard-rank-number', String(index + 1).padStart(2, '0')));
const identity = el('div', 'operations-dashboard-user-identity');
identity.append(
el('span', 'operations-dashboard-avatar', String(item.actor.username || '人').slice(0, 1).toUpperCase()),
@@ -1440,18 +1379,12 @@ function renderOperationsDashboardUsers() {
el('strong', '', item.actor.username),
el('small', '', accountRoleLabel(item.actor.role))
);
const completionRate = operationsDashboardCompletionRate(item.completed, item.total);
const metrics = el('div', 'operations-dashboard-user-metrics');
metrics.append(
el('span', '', `任务 ${item.total}`),
el('span', '', `完成率 ${completionRate}%`),
el('span', item.attention || item.failed ? 'needs-attention' : '', `待关注 ${Number(item.attention || 0) + Number(item.failed || 0)}`)
el('span', '', `操作 ${Number(item.total || 0)}`),
el('span', 'is-success', `成功 ${Number(item.completed || 0)}`)
);
const contribution = el('span', 'operations-dashboard-user-contribution');
const contributionFill = el('i', '');
contributionFill.style.width = `${Math.max(5, (Number(item.total || 0) / maxTotal) * 100)}%`;
contribution.append(contributionFill);
button.append(identity, metrics, contribution, el('small', 'operations-dashboard-user-last', `最近工作 ${formatDateTime(item.last_activity_at)}`));
button.append(identity, metrics);
container.append(button);
}
}
@@ -1462,40 +1395,43 @@ function renderOperationsDashboardTasks() {
container.replaceChildren();
const tasks = operationsDashboardData?.tasks || [];
const total = Number(operationsDashboardData?.total || 0);
$('#operationsDashboardTaskCount').textContent = `${total} 项工`;
$('#operationsDashboardTaskCount').textContent = `${total} 条任务操`;
if (!tasks.length) {
container.append(el('p', 'operations-dashboard-empty', '当前筛选范围内没有工作记录。'));
const emptyRow = el('tr', 'operations-dashboard-table-empty');
const emptyCell = el('td', '');
emptyCell.colSpan = 6;
emptyCell.append(el('p', 'operations-dashboard-empty', '当前筛选范围内没有任务操作记录。'));
emptyRow.append(emptyCell);
container.append(emptyRow);
}
for (const task of tasks) {
const button = el('button', `operations-dashboard-task-row${operationsDashboardSelectedTaskId === task.task_id ? ' is-selected' : ''}`);
button.type = 'button';
button.dataset.operationsTaskId = task.task_id;
const identity = el('span', 'operations-dashboard-task-identity');
const person = el('span', 'operations-dashboard-task-person');
person.append(
const row = el('tr', `operations-dashboard-table-row${operationsDashboardSelectedTaskId === task.task_id ? ' is-selected' : ''}`);
row.dataset.operationsTaskId = task.task_id;
row.tabIndex = 0;
row.setAttribute('role', 'button');
row.setAttribute('aria-label', `查看 ${task.creator.username}${operationsDashboardBusinessLabel(task.business_route_label)}详情`);
const timeCell = el('td', 'operations-dashboard-table-time');
timeCell.append(el('time', '', formatDateTime(task.created_at)));
const actorCell = el('td', 'operations-dashboard-table-actor');
const actor = el('span', 'operations-dashboard-task-person');
actor.append(
el('span', 'operations-dashboard-avatar', String(task.creator.username || '人').slice(0, 1).toUpperCase()),
el('strong', '', task.creator.username)
);
identity.append(
person,
el('time', '', formatDateTime(task.created_at)),
el('span', 'operations-dashboard-task-type', operationsDashboardBusinessLabel(task.business_route_label)),
el('span', `state ${operationsDashboardStatusAppearance(task.outcome_kind)}`, operationsDashboardOutcomeLabel(task.outcome_kind, task.outcome_label))
);
const flow = el('span', 'operations-dashboard-task-flow');
const instruction = el('span', 'operations-dashboard-flow-block is-input');
instruction.append(
el('small', '', `原始输入${Number(task.input_turn_count || 0) > 1 ? ` · ${task.input_turn_count} 次沟通` : ''}`),
el('span', '', operationsDashboardReadableInput(task.instruction_preview))
);
const result = el('span', 'operations-dashboard-flow-block is-output');
result.append(
el('small', '', '最终输出'),
el('span', '', operationsDashboardReadableResult(task.result_summary, task.outcome_kind))
);
flow.append(instruction, el('span', 'operations-dashboard-flow-arrow', '→'), result);
button.append(identity, flow);
container.append(button);
actorCell.append(actor);
const typeCell = el('td', 'operations-dashboard-table-type', operationsDashboardBusinessLabel(task.business_route_label));
const inputCell = el('td', 'operations-dashboard-table-copy');
inputCell.append(el('span', '', operationsDashboardReadableInput(task.instruction_preview)));
if (Number(task.input_turn_count || 0) > 1) {
inputCell.append(el('small', '', `${task.input_turn_count} 次沟通`));
}
const outputCell = el('td', 'operations-dashboard-table-copy is-output');
outputCell.append(el('span', '', operationsDashboardReadableResult(task.result_summary, task.outcome_kind)));
const statusCell = el('td', 'operations-dashboard-table-status');
statusCell.append(el('span', `state ${operationsDashboardStatusAppearance(task.outcome_kind)}`, operationsDashboardOutcomeLabel(task.outcome_kind, task.outcome_label)));
row.append(timeCell, actorCell, typeCell, inputCell, outputCell, statusCell);
container.append(row);
}
const start = total ? operationsDashboardOffset + 1 : 0;
const end = operationsDashboardOffset + tasks.length;
@@ -1505,7 +1441,6 @@ function renderOperationsDashboardTasks() {
}
function renderOperationsDashboard() {
renderOperationsDashboardActiveFilters();
renderOperationsDashboardSummary();
renderOperationsDashboardBreakdowns();
renderOperationsDashboardUsers();
@@ -5535,14 +5470,6 @@ document.addEventListener('DOMContentLoaded', async () => {
$('#operationsDashboardStatus').value = button.dataset.operationsStatus || 'all';
refreshOperationsDashboardAfterDrilldown();
});
$('#operationsDashboardDays')?.addEventListener('click', (event) => {
const button = event.target.closest('[data-operations-day]');
if (!button?.dataset.operationsDay) return;
$('#operationsDashboardFrom').value = button.dataset.operationsDay;
$('#operationsDashboardTo').value = button.dataset.operationsDay;
for (const preset of document.querySelectorAll('[data-operations-range-days]')) preset.classList.remove('is-active');
refreshOperationsDashboardAfterDrilldown();
});
$('#operationsDashboardBusinesses')?.addEventListener('click', (event) => {
const button = event.target.closest('[data-operations-business]');
if (!button?.dataset.operationsBusiness) return;
@@ -5550,13 +5477,20 @@ document.addEventListener('DOMContentLoaded', async () => {
refreshOperationsDashboardAfterDrilldown();
});
$('#operationsDashboardTasks')?.addEventListener('click', (event) => {
const button = event.target.closest('[data-operations-task-id]');
if (!button) return;
void loadOperationsDashboardTask(button.dataset.operationsTaskId).catch((error) => {
const row = event.target.closest('[data-operations-task-id]');
if (!row) return;
void loadOperationsDashboardTask(row.dataset.operationsTaskId).catch((error) => {
$('#operationsDashboardMessage').textContent = operationsDashboardSafeError(error, '工作详情暂时无法打开,请稍后再试。');
closeOperationsDashboardDetail();
});
});
$('#operationsDashboardTasks')?.addEventListener('keydown', (event) => {
if (event.key !== 'Enter' && event.key !== ' ') return;
const row = event.target.closest('[data-operations-task-id]');
if (!row) return;
event.preventDefault();
row.click();
});
$('#operationsDashboardPrevious')?.addEventListener('click', () => {
if (operationsDashboardOffset <= 0 || operationsDashboardBusy) return;
operationsDashboardOffset = Math.max(0, operationsDashboardOffset - OPERATIONS_DASHBOARD_PAGE_SIZE);

View File

@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#edf2f4">
<title>AI操作台 · LianSyn-platform</title>
<link rel="stylesheet" href="styles.css?v=20260901-business-authorization-1">
<link rel="stylesheet" href="styles.css?v=20260902-compact-dashboard-1">
</head>
<body>
<main class="app-shell">
@@ -129,91 +129,84 @@
<section id="operationsDashboardPage" class="operations-dashboard-page" hidden>
<section class="operations-dashboard-panel">
<header class="operations-dashboard-hero">
<div class="operations-dashboard-hero-copy">
<p class="operations-dashboard-kicker">团队运行总览</p>
<h1>平台运行全景</h1>
<p>从整体表现进入每一项工作:看任务规模、人员贡献、业务分布,以及每次原始输入最终形成了什么结果。</p>
</div>
<div class="operations-dashboard-hero-side">
<span id="operationsDashboardFreshness">数据正在更新</span>
<button id="operationsDashboardRefresh" type="button" class="operations-dashboard-refresh">刷新数据</button>
</div>
</header>
<form id="operationsDashboardFilters" class="operations-dashboard-filters">
<div id="operationsDashboardRangePresets" class="operations-dashboard-range-presets" aria-label="快捷时间范围">
<button type="button" data-operations-range-days="7">近 7 天</button>
<button id="operationsDashboardReset" type="button" data-operations-range-days="30" class="is-active">近 30 天</button>
<button type="button" data-operations-range-days="90">近 90 天</button>
</div>
<label>开始日期<input id="operationsDashboardFrom" type="date" required></label>
<label>结束日期<input id="operationsDashboardTo" type="date" required></label>
<label>人员<select id="operationsDashboardActor"><option value="">全部人员</option></select></label>
<label>任务类型<select id="operationsDashboardBusiness"><option value="">全部类型</option></select></label>
<label>完成情况<select id="operationsDashboardStatus">
<option value="all">全部进展</option>
<option value="active">进行中</option>
<option value="completed">已完成</option>
<option value="attention">待跟进</option>
<option value="failed">未完成</option>
<option value="cancelled">已取消</option>
</select></label>
<label class="operations-dashboard-search">查找任务<input id="operationsDashboardSearch" maxlength="200" placeholder="输入人员、原始指令或最终结果"></label>
<button type="submit" class="operations-dashboard-submit">应用筛选</button>
</form>
<div id="operationsDashboardActiveFilters" class="operations-dashboard-active-filters" aria-live="polite"></div>
<div id="operationsDashboardSummary" class="operations-dashboard-summary" aria-live="polite"></div>
<div class="operations-dashboard-insight-grid">
<section class="operations-dashboard-insight operations-dashboard-trend" aria-labelledby="operationsDashboardDaysTitle">
<div class="operations-dashboard-section-head">
<div>
<p>时间趋势</p>
<h2 id="operationsDashboardDaysTitle">每天完成了多少工作</h2>
</div>
<span id="operationsDashboardTodayCount">今天 0 项</span>
</div>
<div id="operationsDashboardDays" class="operations-dashboard-day-chart" aria-label="任务时间趋势"></div>
</section>
<div class="operations-dashboard-ranking-grid">
<section class="operations-dashboard-insight operations-dashboard-business" aria-labelledby="operationsDashboardBusinessesTitle">
<div class="operations-dashboard-section-head">
<div>
<p>任务类型</p>
<h2 id="operationsDashboardBusinessesTitle">团队主要在做什么</h2>
<p>业务分布</p>
<h2 id="operationsDashboardBusinessesTitle">任务类型排名</h2>
</div>
<span>点击类型查看明细</span>
<span>按操作次数排序</span>
</div>
<div id="operationsDashboardBusinesses" class="operations-dashboard-business-list"></div>
</section>
</div>
<section class="operations-dashboard-people" aria-labelledby="operationsDashboardUsersTitle">
<div class="operations-dashboard-section-head">
<div>
<p>人员表现</p>
<h2 id="operationsDashboardUsersTitle">谁在推进哪些工作</h2>
<section class="operations-dashboard-people" aria-labelledby="operationsDashboardUsersTitle">
<div class="operations-dashboard-section-head">
<div>
<p>员工操作</p>
<h2 id="operationsDashboardUsersTitle">员工操作任务排名</h2>
</div>
<span id="operationsDashboardUserCount"></span>
</div>
<span id="operationsDashboardUserCount"></span>
</div>
<div id="operationsDashboardUsers" class="operations-dashboard-users"></div>
</section>
<div id="operationsDashboardUsers" class="operations-dashboard-users"></div>
</section>
</div>
<section class="operations-dashboard-workstream" aria-labelledby="operationsDashboardTasksTitle">
<div class="operations-dashboard-section-head operations-dashboard-workstream-head">
<div>
<p>工作明细</p>
<h2 id="operationsDashboardTasksTitle">从原始输入到最终输出</h2>
<p>筛选与明细</p>
<h2 id="operationsDashboardTasksTitle">任务操作明细</h2>
</div>
<div class="operations-dashboard-update-state">
<span id="operationsDashboardFreshness">数据正在更新</span>
<button id="operationsDashboardRefresh" type="button" class="operations-dashboard-refresh">刷新</button>
</div>
</div>
<form id="operationsDashboardFilters" class="operations-dashboard-filters">
<div id="operationsDashboardRangePresets" class="operations-dashboard-range-presets" aria-label="快捷时间范围">
<button type="button" data-operations-range-days="7">近 7 天</button>
<button id="operationsDashboardReset" type="button" data-operations-range-days="30" class="is-active">近 30 天</button>
<button type="button" data-operations-range-days="90">近 90 天</button>
</div>
<label>开始日期<input id="operationsDashboardFrom" type="date" required></label>
<label>结束日期<input id="operationsDashboardTo" type="date" required></label>
<label>员工名称<select id="operationsDashboardActor"><option value="">全部员工</option></select></label>
<label>任务类型<select id="operationsDashboardBusiness"><option value="">全部类型</option></select></label>
<label>任务结果<select id="operationsDashboardStatus">
<option value="all">全部结果</option>
<option value="active">进行中</option>
<option value="completed">成功</option>
<option value="attention">待跟进</option>
<option value="failed">未成功</option>
<option value="cancelled">已取消</option>
</select></label>
<label class="operations-dashboard-search">关键词<input id="operationsDashboardSearch" maxlength="200" placeholder="员工名称、原始输入或最终输出"></label>
<button type="submit" class="operations-dashboard-submit">查询</button>
</form>
<div class="operations-dashboard-table-meta">
<span id="operationsDashboardTaskCount"></span>
<span>点击任意一行查看完整输入与输出</span>
</div>
<div class="operations-dashboard-flow-labels" aria-hidden="true">
<span>人员与时间</span>
<span>原始输入</span>
<span>最终输出</span>
<div class="operations-dashboard-table-wrap">
<table class="operations-dashboard-table">
<thead>
<tr>
<th scope="col">操作时间</th>
<th scope="col">员工名称</th>
<th scope="col">任务类型</th>
<th scope="col">原始输入</th>
<th scope="col">最终输出</th>
<th scope="col">任务结果</th>
</tr>
</thead>
<tbody id="operationsDashboardTasks"></tbody>
</table>
</div>
<div id="operationsDashboardTasks" class="operations-dashboard-tasks"></div>
<div class="history-pagination operations-dashboard-pagination">
<span id="operationsDashboardPageInfo"></span>
<div class="history-pagination-actions">
@@ -221,8 +214,8 @@
<button id="operationsDashboardNext" type="button" class="secondary-button">下一页</button>
</div>
</div>
<p id="operationsDashboardMessage" class="operations-dashboard-message" role="status"></p>
</section>
<p id="operationsDashboardMessage" class="operations-dashboard-message" role="status"></p>
</section>
<aside id="operationsDashboardDetail" class="operations-dashboard-detail" hidden aria-labelledby="operationsDashboardDetailTitle">
@@ -405,6 +398,6 @@
</section>
</main>
<script src="app.js?v=20260901-business-authorization-1"></script>
<script src="app.js?v=20260902-compact-dashboard-1"></script>
</body>
</html>

View File

@@ -3104,75 +3104,6 @@ textarea {
box-shadow: 0 18px 44px rgba(27, 59, 51, 0.09);
}
.operations-dashboard-hero {
position: relative;
min-height: 156px;
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 32px;
overflow: hidden;
padding: 26px 28px 28px;
border-radius: 10px;
background: var(--dashboard-ink);
color: #f8fbf9;
}
.operations-dashboard-hero::after {
position: absolute;
inset: 0;
background-image: repeating-linear-gradient(90deg, transparent 0, transparent 77px, rgba(255, 255, 255, 0.035) 78px);
content: "";
pointer-events: none;
}
.operations-dashboard-hero-copy,
.operations-dashboard-hero-side {
position: relative;
z-index: 1;
}
.operations-dashboard-hero-copy {
display: grid;
gap: 8px;
}
.operations-dashboard-kicker {
color: #9fc9bb;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.14em;
}
.operations-dashboard-hero h1 {
color: #ffffff;
font-size: clamp(30px, 3vw, 42px);
font-weight: 750;
line-height: 1.02;
letter-spacing: -0.04em;
text-wrap: balance;
}
.operations-dashboard-hero-copy > p:last-child {
max-width: 760px;
color: #c6d8d1;
font-size: 14px;
line-height: 1.65;
text-wrap: pretty;
}
.operations-dashboard-hero-side {
display: flex;
align-items: center;
gap: 12px;
white-space: nowrap;
}
.operations-dashboard-hero-side > span {
color: #b8cec6;
font-size: 12px;
}
.operations-dashboard-refresh {
min-height: 38px;
padding: 0 16px;
@@ -4003,13 +3934,6 @@ textarea {
padding: 12px;
}
.operations-dashboard-hero {
min-height: 0;
align-items: flex-start;
flex-direction: column;
padding: 22px;
}
.operations-dashboard-filters {
grid-template-columns: minmax(0, 1fr);
}
@@ -4038,6 +3962,422 @@ textarea {
}
}
/* Compact leadership dashboard: metrics, two fixed rankings, then filters and detail. */
.operations-dashboard-panel {
gap: 12px;
padding: 16px;
}
.operations-dashboard-summary {
gap: 8px;
}
.operations-dashboard-stat {
min-height: 88px;
gap: 4px;
padding: 12px 14px;
}
.operations-dashboard-stat > strong {
font-size: clamp(25px, 2vw, 32px);
}
.operations-dashboard-stat > small {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.operations-dashboard-stat-success::before,
.operations-dashboard-stat-active::before,
.operations-dashboard-stat-attention::before,
.operations-dashboard-stat-failed::before {
top: 12px;
right: 12px;
}
.operations-dashboard-ranking-grid {
min-width: 0;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.operations-dashboard-ranking-grid .operations-dashboard-insight,
.operations-dashboard-ranking-grid .operations-dashboard-people {
height: 304px;
min-height: 304px;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
gap: 10px;
padding: 14px;
overflow: hidden;
}
.operations-dashboard-ranking-grid .operations-dashboard-section-head {
align-items: center;
}
.operations-dashboard-ranking-grid .operations-dashboard-section-head h2 {
font-size: 16px;
}
.operations-dashboard-business-list,
.operations-dashboard-users {
min-width: 0;
min-height: 0;
display: grid;
grid-auto-flow: row;
grid-auto-columns: auto;
align-content: start;
gap: 5px;
overflow-x: hidden;
overflow-y: auto;
padding: 0 3px 0 0;
}
.operations-dashboard-business-row,
.operations-dashboard-user-row {
width: 100%;
min-width: 0;
min-height: 42px;
display: grid;
grid-template-columns: 34px minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
padding: 7px 10px;
border: 0;
border-radius: 6px;
background: #f4f6f3;
color: var(--dashboard-ink);
font: inherit;
text-align: left;
cursor: pointer;
}
.operations-dashboard-business-row:hover,
.operations-dashboard-user-row:hover {
background: #eaf2ee;
transform: none;
}
.operations-dashboard-rank-number {
color: #87938e;
font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
font-size: 11px;
font-weight: 700;
letter-spacing: -0.03em;
}
.operations-dashboard-rank-name {
overflow: hidden;
color: var(--dashboard-ink);
font-size: 12px;
font-weight: 700;
text-overflow: ellipsis;
white-space: nowrap;
}
.operations-dashboard-rank-metrics,
.operations-dashboard-user-metrics {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
color: var(--dashboard-muted);
font-size: 11px;
white-space: nowrap;
}
.operations-dashboard-rank-metrics .is-success,
.operations-dashboard-user-metrics .is-success {
color: var(--dashboard-accent);
font-weight: 700;
}
.operations-dashboard-user-row {
grid-template-columns: 34px minmax(0, 1fr) auto;
}
.operations-dashboard-user-identity {
gap: 8px;
}
.operations-dashboard-user-row .operations-dashboard-avatar {
width: 28px;
height: 28px;
border-radius: 6px;
font-size: 11px;
}
.operations-dashboard-user-name strong {
font-size: 12px;
}
.operations-dashboard-workstream {
min-height: 480px;
grid-template-rows: auto auto auto minmax(280px, 1fr) auto auto;
gap: 10px;
padding: 14px;
}
.operations-dashboard-workstream-head {
align-items: center;
}
.operations-dashboard-update-state {
display: flex;
align-items: center;
gap: 10px;
color: var(--dashboard-muted);
font-size: 11px;
white-space: nowrap;
}
.operations-dashboard-refresh {
min-height: 32px;
padding: 0 12px;
border: 1px solid var(--dashboard-line);
border-radius: 6px;
background: #ffffff;
color: var(--dashboard-ink-soft);
font-size: 11px;
font-weight: 700;
}
.operations-dashboard-refresh:hover {
border-color: #9cb4aa;
background: #eef4f1;
}
.operations-dashboard-filters {
grid-template-columns: auto repeat(5, minmax(118px, 0.72fr)) minmax(220px, 1.35fr) auto;
gap: 8px;
padding: 11px;
border-radius: 7px;
background: #f5f7f4;
}
.operations-dashboard-filters label {
gap: 5px;
font-size: 10px;
}
.operations-dashboard-filters input,
.operations-dashboard-filters select,
.operations-dashboard-submit {
min-height: 38px;
}
.operations-dashboard-range-presets {
align-self: end;
}
.operations-dashboard-range-presets button {
min-height: 30px;
padding: 0 9px;
}
.operations-dashboard-table-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
color: var(--dashboard-muted);
font-size: 11px;
}
.operations-dashboard-table-meta > span:first-child {
color: var(--dashboard-ink-soft);
font-weight: 700;
}
.operations-dashboard-table-wrap {
min-width: 0;
min-height: 280px;
overflow: auto;
border: 1px solid var(--dashboard-line);
border-radius: 7px;
background: #ffffff;
}
.operations-dashboard-table {
width: 100%;
min-width: 1120px;
border-collapse: separate;
border-spacing: 0;
table-layout: fixed;
color: var(--dashboard-ink);
font-size: 12px;
}
.operations-dashboard-table th {
position: sticky;
z-index: 1;
top: 0;
padding: 10px 12px;
border-bottom: 1px solid var(--dashboard-line);
background: #eef2ef;
color: var(--dashboard-muted);
font-size: 10px;
font-weight: 750;
letter-spacing: 0.04em;
text-align: left;
white-space: nowrap;
}
.operations-dashboard-table th:nth-child(1) {
width: 148px;
}
.operations-dashboard-table th:nth-child(2) {
width: 132px;
}
.operations-dashboard-table th:nth-child(3) {
width: 170px;
}
.operations-dashboard-table th:nth-child(6) {
width: 94px;
}
.operations-dashboard-table-row {
cursor: pointer;
outline: none;
transition: background-color 160ms ease, box-shadow 160ms ease;
}
.operations-dashboard-table-row td {
height: 66px;
padding: 9px 12px;
overflow: hidden;
border-bottom: 1px solid #e4e9e6;
vertical-align: middle;
}
.operations-dashboard-table-row:last-child td {
border-bottom: 0;
}
.operations-dashboard-table-row:hover td,
.operations-dashboard-table-row:focus-visible td,
.operations-dashboard-table-row.is-selected td {
background: #eef5f1;
}
.operations-dashboard-table-row:focus-visible {
box-shadow: inset 3px 0 0 var(--dashboard-accent);
}
.operations-dashboard-table-time time {
color: var(--dashboard-muted);
font-size: 11px;
white-space: nowrap;
}
.operations-dashboard-table-actor .operations-dashboard-avatar {
width: 28px;
height: 28px;
border-radius: 6px;
font-size: 11px;
}
.operations-dashboard-table-actor strong,
.operations-dashboard-table-type {
overflow: hidden;
font-size: 11px;
font-weight: 700;
text-overflow: ellipsis;
white-space: nowrap;
}
.operations-dashboard-table-copy > span {
display: -webkit-box;
overflow: hidden;
color: var(--dashboard-ink-soft);
font-size: 11px;
line-height: 1.45;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.operations-dashboard-table-copy.is-output > span {
color: #286251;
}
.operations-dashboard-table-copy > small {
display: block;
margin-top: 3px;
color: var(--dashboard-muted);
font-size: 9px;
}
.operations-dashboard-table-status .state {
display: inline-flex;
padding: 5px 7px;
font-size: 10px;
white-space: nowrap;
}
.operations-dashboard-table-empty td {
padding: 0;
}
.operations-dashboard-pagination {
padding-top: 8px;
}
.operations-dashboard-message:empty {
min-height: 0;
}
@media (max-width: 1450px) {
.operations-dashboard-filters {
grid-template-columns: auto repeat(3, minmax(150px, 1fr));
}
.operations-dashboard-search {
grid-column: span 2;
}
}
@media (max-width: 980px) {
.operations-dashboard-ranking-grid {
grid-template-columns: minmax(0, 1fr);
}
.operations-dashboard-ranking-grid .operations-dashboard-insight,
.operations-dashboard-ranking-grid .operations-dashboard-people {
height: 280px;
min-height: 280px;
}
}
@media (max-width: 760px) {
.operations-dashboard-panel {
padding: 10px;
}
.operations-dashboard-summary {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.operations-dashboard-filters {
grid-template-columns: minmax(0, 1fr);
}
.operations-dashboard-search {
grid-column: auto;
}
.operations-dashboard-table-meta,
.operations-dashboard-workstream-head {
align-items: flex-start;
flex-direction: column;
}
}
.history-page .workbench-grid {
grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
}