feat: compact leadership dashboard layout
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user