feat: 添加进程表排序功能,优化用户交互体验

This commit is contained in:
cmliu committed 2026-07-29 20:23:40 +08:00
1 parent 339f4ce53d
commit 5bc81940d3
3 files changed
+115 -19

No files matched your search

+7 -7
View File
@@ -456,13 +456,13 @@
<table class="process-table" aria-describedby="process-manager-status">
<thead>
<tr>
<th scope="col">PID</th>
<th scope="col" data-i18n-zh="用户" data-i18n-en="User">用户</th>
<th scope="col" data-i18n-zh="内存" data-i18n-en="Memory">内存</th>
<th scope="col">MEM%</th>
<th scope="col">CPU%</th>
<th scope="col" data-i18n-zh="状态" data-i18n-en="State">状态</th>
<th scope="col" data-i18n-zh="时间" data-i18n-en="Time">时间</th>
<th scope="col" aria-sort="none"><button class="process-sort" type="button" data-process-sort="pid" data-i18n-zh="PID" data-i18n-en="PID">PID</button></th>
<th scope="col" aria-sort="none"><button class="process-sort" type="button" data-process-sort="user" data-i18n-zh="用户" data-i18n-en="User">用户</button></th>
<th scope="col" aria-sort="none"><button class="process-sort" type="button" data-process-sort="memoryBytes" data-i18n-zh="内存" data-i18n-en="Memory">内存</button></th>
<th scope="col" aria-sort="none"><button class="process-sort" type="button" data-process-sort="memoryPercent" data-i18n-zh="MEM%" data-i18n-en="MEM%">MEM%</button></th>
<th scope="col" aria-sort="descending"><button class="process-sort" type="button" data-process-sort="cpuPercent" data-i18n-zh="CPU%" data-i18n-en="CPU%">CPU%</button></th>
<th scope="col" aria-sort="none"><button class="process-sort" type="button" data-process-sort="state" data-i18n-zh="状态" data-i18n-en="State">状态</button></th>
<th scope="col" aria-sort="none"><button class="process-sort" type="button" data-process-sort="time" data-i18n-zh="时间" data-i18n-en="Time">时间</button></th>
<th scope="col" data-i18n-zh="名称/命令行" data-i18n-en="Name / command">名称/命令行</th>
</tr>
</thead>
+68 -1
View File
@@ -33,6 +33,9 @@ interface ProcessEntry {
command: string;
}
type ProcessSortKey = 'pid' | 'user' | 'memoryBytes' | 'memoryPercent' | 'cpuPercent' | 'state' | 'time';
type SortDirection = 'ascending' | 'descending';
interface ProcessSnapshot {
type: 'process_snapshot';
metrics: {
@@ -135,12 +138,19 @@ export class ProcessManager {
private socket: WebSocket | null = null;
private generation = 0;
private snapshot: ProcessSnapshot | null = null;
private sortKey: ProcessSortKey = 'cpuPercent';
private sortDirection: SortDirection = 'descending';
private readonly progressAnimations = new Map<HTMLProgressElement, number>();
constructor(options: ProcessManagerOptions) {
this.elements = options.elements;
this.getLanguage = options.getLanguage;
this.onError = options.onError;
this.elements.panel.addEventListener('click', (event) => {
const button = (event.target as HTMLElement).closest<HTMLButtonElement>('[data-process-sort]');
if (!button) return;
this.changeSort(button.dataset.processSort as ProcessSortKey);
});
this.render();
}
@@ -183,9 +193,66 @@ export class ProcessManager {
}
setLanguage(): void {
this.updateSortHeaders();
this.render();
}
private changeSort(key: ProcessSortKey): void {
if (this.sortKey === key) {
this.sortDirection = this.sortDirection === 'ascending' ? 'descending' : 'ascending';
} else {
this.sortKey = key;
this.sortDirection = key === 'user' || key === 'state' ? 'ascending' : 'descending';
}
this.updateSortHeaders();
this.render();
}
private updateSortHeaders(): void {
for (const button of this.elements.panel.querySelectorAll<HTMLButtonElement>('[data-process-sort]')) {
const key = button.dataset.processSort as ProcessSortKey;
const header = button.closest<HTMLTableCellElement>('th');
const active = key === this.sortKey;
header?.setAttribute('aria-sort', active ? this.sortDirection : 'none');
const label = button.dataset[this.getLanguage() === 'zh-CN' ? 'i18nZh' : 'i18nEn'] ?? button.textContent ?? '';
button.setAttribute('aria-label', active
? this.getLanguage() === 'zh-CN'
? `${label},当前${this.sortDirection === 'ascending' ? '升序' : '降序'},点击切换排序`
: `${label}, currently ${this.sortDirection}; activate to reverse`
: this.getLanguage() === 'zh-CN'
? `按 ${label} 排序`
: `Sort by ${label}`);
}
}
private sortedProcesses(processes: ProcessEntry[]): ProcessEntry[] {
const key = this.sortKey;
const direction = this.sortDirection === 'ascending' ? 1 : -1;
const value = (process: ProcessEntry): string | number | null => {
if (key === 'time') return this.processTimeSeconds(process.time);
return process[key];
};
return processes.map((process, index) => ({ process, index })).sort((left, right) => {
const a = value(left.process);
const b = value(right.process);
if (a === null && b === null) return left.index - right.index;
if (a === null) return 1;
if (b === null) return -1;
const compared = typeof a === 'number' && typeof b === 'number'
? a - b
: String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: 'base' });
return compared === 0 ? left.index - right.index : compared * direction;
}).map(({ process }) => process);
}
private processTimeSeconds(value: string): number | null {
const parts = value.replace(',', '.').split(':').map(Number);
if (parts.some((part) => !Number.isFinite(part))) return null;
if (parts.length === 3) return (parts[0] * 3600) + (parts[1] * 60) + parts[2];
if (parts.length === 2) return (parts[0] * 60) + parts[1];
return parts.length === 1 ? parts[0] : null;
}
private handleMessage(serialized: string): void {
let message: unknown;
try { message = JSON.parse(serialized); } catch { return; }
@@ -219,7 +286,7 @@ export class ProcessManager {
}
const fragment = document.createDocumentFragment();
for (const process of snapshot.processes) {
for (const process of this.sortedProcesses(snapshot.processes)) {
const row = document.createElement('tr');
const values = [
String(process.pid),
+40 -11
View File
@@ -1375,18 +1375,46 @@ button:disabled {
color: var(--muted);
font-family: var(--sans);
font-size: 9px;
text-align: left;
text-align: right;
white-space: nowrap;
}
.process-table th:first-child { width: 8%; }
.process-table th:nth-child(2) { width: 11%; }
.process-table th:nth-child(3) { width: 10%; }
.process-table th:nth-child(2) { width: 8%; }
.process-table th:nth-child(3) { width: 8%; }
.process-table th:nth-child(4),
.process-table th:nth-child(5) { width: 8%; }
.process-table th:nth-child(6) { width: 7%; }
.process-table th:nth-child(7) { width: 10%; }
.process-table th:nth-child(8) { width: 38%; }
.process-table th:nth-child(5) { width: 7%; }
.process-table th:nth-child(6) { width: 6%; }
.process-table th:nth-child(7) { width: 9%; }
.process-table th:nth-child(8) { width: 47%; text-align: left; }
.process-sort {
position: relative;
width: 100%;
height: 25px;
padding: 0 11px 0 0;
border: 0;
background: transparent;
color: inherit;
font: inherit;
font-weight: 700;
text-align: right;
}
.process-sort::after {
position: absolute;
right: 0;
color: var(--muted-2);
content: '↕';
}
.process-table th[aria-sort='ascending'] .process-sort,
.process-table th[aria-sort='descending'] .process-sort {
color: var(--accent);
}
.process-table th[aria-sort='ascending'] .process-sort::after { content: '↑'; }
.process-table th[aria-sort='descending'] .process-sort::after { content: '↓'; }
.process-table td {
height: 24px;
@@ -1397,13 +1425,14 @@ button:disabled {
white-space: nowrap;
}
.process-table td:first-child,
.process-table td:nth-child(3),
.process-table td:nth-child(4),
.process-table td:nth-child(5) {
.process-table td:not(:last-child) {
text-align: right;
}
.process-table td:last-child {
text-align: left;
}
.process-table tbody tr:hover {
background: var(--surface-2);
}