diff --git a/frontend/index.html b/frontend/index.html
index afe50cf..6c2e7b6 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -456,13 +456,13 @@
- | PID |
- 用户 |
- 内存 |
- MEM% |
- CPU% |
- 状态 |
- 时间 |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
名称/命令行 |
diff --git a/frontend/src/process-manager.ts b/frontend/src/process-manager.ts
index 0963689..122add3 100644
--- a/frontend/src/process-manager.ts
+++ b/frontend/src/process-manager.ts
@@ -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();
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('[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('[data-process-sort]')) {
+ const key = button.dataset.processSort as ProcessSortKey;
+ const header = button.closest('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),
diff --git a/frontend/src/style.css b/frontend/src/style.css
index 03ac2b2..3ea4003 100644
--- a/frontend/src/style.css
+++ b/frontend/src/style.css
@@ -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);
}