feat: compact leadership dashboard layout
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
# Task: Compact leadership dashboard layout
|
||||
|
||||
## Identity
|
||||
|
||||
- Task ID: 20260902-compact-dashboard-layout-b84f2d
|
||||
- Mode: Feature
|
||||
- Branch: main
|
||||
- Worktree: /Users/inmanx/Documents/lwltAPI
|
||||
- Base commit: 3ed3af1feb503358b98fb57d3e8d97ab59d98129
|
||||
- Owner: codex
|
||||
- Status: Ready for integration
|
||||
|
||||
## Scope
|
||||
|
||||
- Remove the entire “平台运行全景” top banner from `/operations-dashboard` so the leadership view starts with the indicator row.
|
||||
- Place compact, fixed-layout task-type and employee-operation rankings directly below the indicators.
|
||||
- Rank both dimensions by operation count and show operation count plus success count only; remove completion-rate presentation.
|
||||
- Place filter parameters and the task detail table below the rankings, including an explicit employee-name filter.
|
||||
- Preserve the existing leadership authorization boundary, business-safe task projection, and read-only drill-through behavior.
|
||||
|
||||
## Intent And Constraints
|
||||
|
||||
- The page is a leadership operations view, not a technical audit or engineering diagnostics screen.
|
||||
- Visible ranking metrics must be counts, never completion percentages; the two ranking panels should remain visually stable even when their row counts differ.
|
||||
- A leader must be able to move from a KPI or ranking into the same filtered task list, then open one row to see the employee, original input, and final output.
|
||||
- Keep the current vanilla HTML/CSS/JavaScript stack and the current operations-dashboard API; do not redesign account authorization, modify task data, access ERP, or add an organization concept.
|
||||
- Do not restart or deploy the service without a separate explicit authorization.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Deleted the full top hero/banner and its live CSS, including the “平台运行全景” heading and explanatory copy. The first dashboard content is now the six-card indicator row.
|
||||
- Removed the prior time-trend block and rebuilt the next row as two equal, compact ranking panels: “任务类型排名” and “员工操作任务排名”. Both use a fixed 304px panel height with internal scrolling when needed.
|
||||
- Simplified every ranking row to ordinal, identity/type, operation count, and success count. No completion rate or percentage is calculated or rendered.
|
||||
- Moved the date presets, date inputs, employee name, task type, task result, and keyword query controls into the section immediately above the task details.
|
||||
- Replaced the prior task-flow cards with a semantic detail table containing operation time, employee name, task type, original input, final output, and task result. Rows remain keyboard- and pointer-openable for the complete read-only detail drawer.
|
||||
- Kept status KPI drill-through, task-type drill-through, employee drill-through, pagination, refresh, date ranges, business-safe input/output text, and the existing `admin`/`team_lead` visibility gate.
|
||||
- Updated cache-bust references and regression assertions for the compact dashboard presentation.
|
||||
- The already-running 8786 service serves these static page changes immediately. No service restart, deployment, database mutation, task mutation, ERP access, or external send was performed.
|
||||
|
||||
## Verification
|
||||
|
||||
- `node --check LianSyn-platform/app.js`: passed with the bundled Node runtime.
|
||||
- Focused account/dashboard regression: 8/8 passed.
|
||||
- `node --run check:repo`: 10/10 passed.
|
||||
- `node --run check`: passed.
|
||||
- `node --run test:control-plane`: 153/153 passed.
|
||||
- `node --run test:legacy`: 256/256 passed.
|
||||
- `node --run build`: passed.
|
||||
- `git diff --check`: passed.
|
||||
- Authenticated browser verification on `http://127.0.0.1:8786/operations-dashboard?layout=compact-1` confirmed that the KPI row is first, the hero is absent, the two rankings are equal-height and compact, and only operation/success counts appear in ranking rows.
|
||||
- Browser data verification loaded 359 task operations, 251 successes, 23 task types, and 1 participating employee in the current 30-day range.
|
||||
- Browser interaction verification selected employee `lwltadmin`, kept the expected 359 matching operations, drilled the “删除订单” ranking into 43 matching operations, and opened a table row whose drawer displayed employee, original input, and final output.
|
||||
- Static presentation scan found no `平台运行全景`, `完成率`, completion-rate helper, or removed day-chart renderer in the live dashboard HTML/JavaScript.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- The current data contains one employee, so the fixed employee ranking intentionally shows one row and unused panel space. It will populate without layout changes as additional ordinary users and team leads operate tasks.
|
||||
|
||||
## Promotion Candidates
|
||||
|
||||
- Target: `AUTH-001` and `.project-docs/40-domain/business-rules.md` during a later Integration Gate.
|
||||
- Proposal: record the accepted leadership-dashboard visual order as KPI indicators → compact task-type/employee rankings → filters and task detail table.
|
||||
- Proposal: task-type and employee rankings are count-based only and expose operation count plus success count; completion percentages are not part of leadership ranking semantics.
|
||||
- Evidence: `LianSyn-platform/index.html`, `LianSyn-platform/app.js`, `LianSyn-platform/styles.css`, `control-plane/test/account-authorization.test.ts`, `control-plane/test/control-plane.test.ts`, full regression checks, and authenticated browser verification summarized above.
|
||||
- Future impact: dashboard redesigns and regression tests should preserve the accepted information hierarchy, fixed ranking footprint, employee-name query, and business-language drill-through.
|
||||
- Semantic conflict: this refines the previous aggregate-first dashboard presentation by removing its hero and time trend; it does not change leadership access, task ownership, or API data semantics.
|
||||
- Human confirmation required: received in this task through the explicit requested layout and metric correction.
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -219,16 +219,17 @@ test('operator UI exposes role-aware accounts, executive drill-through, original
|
||||
assert.match(index, /value="team_lead">组长/);
|
||||
assert.match(index, /id="operationsDashboardFilters"/);
|
||||
assert.match(index, /id="operationsDashboardBusiness"/);
|
||||
assert.match(index, /id="operationsDashboardDays"/);
|
||||
assert.match(index, /id="operationsDashboardBusinesses"/);
|
||||
assert.match(index, /平台运行全景/);
|
||||
assert.match(index, /任务规模、人员贡献、业务分布/);
|
||||
assert.match(index, /输入人员、原始指令或最终结果/);
|
||||
assert.match(index, /任务类型排名/);
|
||||
assert.match(index, /员工操作任务排名/);
|
||||
assert.match(index, /员工名称/);
|
||||
assert.match(index, /class="operations-dashboard-table"/);
|
||||
assert.match(index, /员工名称、原始输入或最终输出/);
|
||||
assert.match(index, /id="operationsDashboardRangePresets"/);
|
||||
assert.match(index, /id="operationsDashboardRefresh"/);
|
||||
assert.match(index, /从原始输入到最终输出/);
|
||||
assert.match(index, /任务操作明细/);
|
||||
assert.match(index, /id="operationsDashboardDetail"/);
|
||||
assert.doesNotMatch(index, /OPERATIONS OVERVIEW|BUSINESS TRACE|指令操作历史/);
|
||||
assert.doesNotMatch(index, /平台运行全景|operations-dashboard-hero|OPERATIONS OVERVIEW|BUSINESS TRACE|指令操作历史/);
|
||||
assert.match(index, /id="historyArchiveInput"/);
|
||||
assert.match(app, /authUser\?\.role === 'admin'/);
|
||||
assert.match(app, /crypto\.randomUUID/);
|
||||
@@ -242,8 +243,8 @@ test('operator UI exposes role-aware accounts, executive drill-through, original
|
||||
assert.match(app, /\/api\/operations-dashboard\/tasks\/\$\{encodeURIComponent\(taskId\)\}/);
|
||||
assert.match(app, /business_route_id/);
|
||||
assert.match(app, /data-operations-status|dataset\.operationsStatus/);
|
||||
assert.match(app, /data-operations-day|dataset\.operationsDay/);
|
||||
assert.match(app, /data-operations-business|dataset\.operationsBusiness/);
|
||||
assert.doesNotMatch(app, /完成率|operationsDashboardCompletionRate|operationsDashboardDays/);
|
||||
assert.match(app, /function operationsDashboardReadableResult/);
|
||||
assert.match(app, /function operationsDashboardReadableInput/);
|
||||
assert.match(app, /原始输入/);
|
||||
|
||||
@@ -1225,8 +1225,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');
|
||||
assert.match(index, /id="loginPanel"/);
|
||||
assert.match(index, /id="workbench"[^>]*hidden/);
|
||||
assert.match(index, /styles\.css\?v=20260901-business-authorization-1/);
|
||||
assert.match(index, /app\.js\?v=20260901-business-authorization-1/);
|
||||
assert.match(index, /styles\.css\?v=20260902-compact-dashboard-1/);
|
||||
assert.match(index, /app\.js\?v=20260902-compact-dashboard-1/);
|
||||
assert.match(index, /id="statusDetailsPopover"/);
|
||||
assert.match(index, /id="statusDetailsRefresh"/);
|
||||
assert.match(app, /apiRequest\(`\/api\/tasks\?\$\{params\.toString\(\)\}`/);
|
||||
|
||||
Reference in New Issue
Block a user