feat: 添加进程表排序功能,优化用户交互体验
This commit is contained in:
1 parent
339f4ce53d
commit
5bc81940d3
3 files changed
+115
-19
No files matched your search
+7
-7
@@ -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>
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user