Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 118 additions & 19 deletions plugins/ztools-kill-process/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Header click sort handlers
const thName = document.getElementById('thName');
const thPID = document.getElementById('thPID');
const thPort = document.getElementById('thPort');
const thMem = document.getElementById('thMem');

// Modal Elements
Expand Down Expand Up @@ -88,11 +89,11 @@ document.addEventListener('DOMContentLoaded', () => {
// Resolution of process service from preload
const processService = window.services || window.processService || {
getProcesses: async () => [
{ name: 'chrome.exe', pid: 12100, memoryStr: '132.3 MB', memoryKB: 135480 },
{ name: 'Antigravity.exe', pid: 65176, memoryStr: '252.4 MB', memoryKB: 258488 },
{ name: 'ZTools.exe', pid: 56044, memoryStr: '91.9 MB', memoryKB: 94092 },
{ name: 'WeChatAppEx.exe', pid: 54964, memoryStr: '133.1 MB', memoryKB: 136308 },
{ name: 'Unity.exe', pid: 28976, memoryStr: '0.96 GB', memoryKB: 1002752 }
{ name: 'chrome.exe', pid: 12100, memoryStr: '132.3 MB', memoryKB: 135480, listeningPorts: [8080, 8081], allPorts: [8080, 8081], primaryPort: 8080, portsStr: ':8080, :8081' },
{ name: 'Antigravity.exe', pid: 65176, memoryStr: '252.4 MB', memoryKB: 258488, listeningPorts: [], allPorts: [54321], primaryPort: 54321, portsStr: ':54321' },
{ name: 'ZTools.exe', pid: 56044, memoryStr: '91.9 MB', memoryKB: 94092, listeningPorts: [], allPorts: [], primaryPort: Infinity, portsStr: '' },
{ name: 'WeChatAppEx.exe', pid: 54964, memoryStr: '133.1 MB', memoryKB: 136308, listeningPorts: [], allPorts: [], primaryPort: Infinity, portsStr: '' },
{ name: 'Unity.exe', pid: 28976, memoryStr: '0.96 GB', memoryKB: 1002752, listeningPorts: [3000], allPorts: [3000], primaryPort: 3000, portsStr: ':3000' }
],
killProcess: async (pid) => `Mock killed process PID ${pid}`
};
Expand Down Expand Up @@ -120,12 +121,20 @@ document.addEventListener('DOMContentLoaded', () => {
}, 3000);
}

let isFetchingProcesses = false;
let searchDebounceTimer = null;

// Load and refresh process list
async function loadProcesses(keepIndex = true) {
async function loadProcesses(keepIndex = true, silent = false) {
if (isFetchingProcesses) return;
isFetchingProcesses = true;

adjustPluginHeight(580);
loadingState.style.display = 'flex';
emptyState.style.display = 'none';
processListEl.style.display = 'none';
if (!silent) {
loadingState.style.display = 'flex';
emptyState.style.display = 'none';
processListEl.style.display = 'none';
}

try {
allProcesses = await processService.getProcesses();
Expand All @@ -141,21 +150,44 @@ document.addEventListener('DOMContentLoaded', () => {
applyFilterAndSort(keepIndex);
} catch (err) {
console.error('Failed to load processes:', err);
showToast('获取进程列表失败: ' + (err.message || '未知错误'), 'error');
if (!silent) {
showToast('获取进程列表失败: ' + (err.message || '未知错误'), 'error');
}
} finally {
loadingState.style.display = 'none';
processListEl.style.display = 'block';
if (searchInput && !isModalOpen) {
isFetchingProcesses = false;
if (!silent) {
loadingState.style.display = 'none';
processListEl.style.display = 'block';
}
if (searchInput && !isModalOpen && document.activeElement !== searchInput) {
searchInput.focus();
}
}
}

// Window Focus & Plugin Enter Lifecycle Events for Auto-Refresh
window.onPluginEnter = function(action) {
loadProcesses(true, allProcesses.length > 0);
};

window.addEventListener('focus', () => {
if (!isModalOpen && !isFetchingProcesses) {
loadProcesses(true, true);
}
});

document.addEventListener('visibilitychange', () => {
if (!document.hidden && !isModalOpen && !isFetchingProcesses) {
loadProcesses(true, true);
}
});

// Update header sort direction icons
function updateHeaderSortIcons() {
const headers = [
{ el: thName, field: 'name' },
{ el: thPID, field: 'pid' },
{ el: thPort, field: 'primaryPort' },
{ el: thMem, field: 'memoryKB' }
];

Expand Down Expand Up @@ -209,18 +241,36 @@ document.addEventListener('DOMContentLoaded', () => {

// Filter and Sort logic
function applyFilterAndSort(keepIndex = false) {
const query = searchInput.value.trim().toLowerCase();
const rawQuery = searchInput.value.trim().toLowerCase();

// Filter
if (!query) {
if (!rawQuery) {
filteredProcesses = [...allProcesses];
searchClear.style.display = 'none';
} else {
searchClear.style.display = 'flex';

// Clean query for port matching (e.g. ":8080", "port:8080", "port=8080" -> "8080")
const cleanPortQuery = rawQuery.replace(/^port\s*[:=]?\s*/i, '').replace(/^[:]/, '').trim();

filteredProcesses = allProcesses.filter(p => {
const nameMatch = p.name.toLowerCase().includes(query);
const pidMatch = p.pid.toString().includes(query);
return nameMatch || pidMatch;
const nameMatch = p.name.toLowerCase().includes(rawQuery);
const pidMatch = p.pid.toString().includes(rawQuery);

let portMatch = false;
if (cleanPortQuery) {
const listeningPorts = p.listeningPorts || [];
const allPorts = p.allPorts || [];
const portsStr = p.portsStr || '';

const hasListeningMatch = listeningPorts.some(port => port.toString().includes(cleanPortQuery));
const hasAllMatch = allPorts.some(port => port.toString().includes(cleanPortQuery));
const hasStrMatch = portsStr.toLowerCase().includes(rawQuery) || portsStr.toLowerCase().includes(cleanPortQuery);

portMatch = hasListeningMatch || hasAllMatch || hasStrMatch;
}

return nameMatch || pidMatch || portMatch;
});
}

Expand All @@ -229,6 +279,9 @@ document.addEventListener('DOMContentLoaded', () => {
let valA = a[currentSort.field];
let valB = b[currentSort.field];

if (valA === undefined || valA === null) valA = currentSort.field === 'primaryPort' ? Infinity : '';
if (valB === undefined || valB === null) valB = currentSort.field === 'primaryPort' ? Infinity : '';

if (typeof valA === 'string') valA = valA.toLowerCase();
if (typeof valB === 'string') valB = valB.toLowerCase();

Expand All @@ -251,6 +304,36 @@ document.addEventListener('DOMContentLoaded', () => {
updateBatchKillState();
}

// Render Port Badges Helper
function renderPortBadges(proc) {
const listening = proc.listeningPorts || [];
const all = proc.allPorts || [];

if (listening.length > 0) {
const showPorts = listening.slice(0, 2);
const extraCount = listening.length - showPorts.length;
const fullTooltip = `监听端口: ${listening.map(p => ':' + p).join(', ')}`;

let html = showPorts.map(p => `<span class="port-badge" title="${fullTooltip}">:${p}</span>`).join(' ');
if (extraCount > 0) {
html += ` <span class="port-badge-more" title="${fullTooltip}">+${extraCount}</span>`;
}
return html;
} else if (all.length > 0) {
const showPorts = all.slice(0, 2);
const extraCount = all.length - showPorts.length;
const fullTooltip = `活跃连接端口: ${all.map(p => ':' + p).join(', ')}`;

let html = showPorts.map(p => `<span class="port-badge port-badge-active" title="${fullTooltip}">:${p}</span>`).join(' ');
if (extraCount > 0) {
html += ` <span class="port-badge-more" title="${fullTooltip}">+${extraCount}</span>`;
}
return html;
}

return `<span class="no-port">-</span>`;
}

// Render Process Items
function renderList() {
processListEl.innerHTML = '';
Expand All @@ -268,6 +351,8 @@ document.addEventListener('DOMContentLoaded', () => {
item.className = `process-item ${index === selectedIndex ? 'selected' : ''} ${isChecked ? 'checked-row' : ''}`;
item.setAttribute('data-index', index);

const portHtml = renderPortBadges(proc);

item.innerHTML = `
<div class="checkbox-cell">
<input type="checkbox" class="row-checkbox" data-index="${index}" ${isChecked ? 'checked' : ''}>
Expand All @@ -288,6 +373,7 @@ document.addEventListener('DOMContentLoaded', () => {
<span>${escapeHtml(proc.name)}</span>
</div>
<div class="pid-cell">${proc.pid}</div>
<div class="port-cell">${portHtml}</div>
<div class="mem-cell">${escapeHtml(proc.memoryStr)}</div>
<div class="action-cell">
<button type="button" class="btn-kill-row" data-action="kill" data-index="${index}">
Expand Down Expand Up @@ -435,6 +521,10 @@ document.addEventListener('DOMContentLoaded', () => {
<span class="info-label">进程 PID:</span>
<span class="info-value">${proc.pid}</span>
</div>
<div class="info-row">
<span class="info-label">占用端口:</span>
<span class="info-value" style="color: var(--accent-green); font-family: var(--font-mono);">${escapeHtml(proc.portsStr || '无')}</span>
</div>
<div class="info-row">
<span class="info-label">内存占用:</span>
<span class="info-value">${escapeHtml(proc.memoryStr)}</span>
Expand All @@ -459,7 +549,7 @@ document.addEventListener('DOMContentLoaded', () => {

let html = selectedProcs.map(p => `
<div class="modal-proc-tag">
<span><strong>${escapeHtml(p.name)}</strong> (PID: ${p.pid})</span>
<span><strong>${escapeHtml(p.name)}</strong> (PID: ${p.pid}${p.portsStr ? ' | ' + escapeHtml(p.portsStr) : ''})</span>
<span style="color: #38bdf8; font-family: var(--font-mono); font-size: 11px;">${p.memoryStr}</span>
</div>
`).join('');
Expand Down Expand Up @@ -647,12 +737,20 @@ document.addEventListener('DOMContentLoaded', () => {

// Search Input Event
searchInput.addEventListener('input', () => {
// 1. Instantly filter existing cached processes for fast response
applyFilterAndSort(false);

// 2. Debounce (300ms) background refresh from OS to catch newly launched processes/ports
if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
searchDebounceTimer = setTimeout(() => {
loadProcesses(true, true);
}, 300);
});

searchClear.addEventListener('click', () => {
searchInput.value = '';
applyFilterAndSort(false);
loadProcesses(true, true);
searchInput.focus();
});

Expand All @@ -674,6 +772,7 @@ document.addEventListener('DOMContentLoaded', () => {

thName.addEventListener('click', () => toggleSort('name', false));
thPID.addEventListener('click', () => toggleSort('pid', false));
if (thPort) thPort.addEventListener('click', () => toggleSort('primaryPort', false));
thMem.addEventListener('click', () => toggleSort('memoryKB', true));

// Modal Mouse Click Handlers
Expand Down
47 changes: 45 additions & 2 deletions plugins/ztools-kill-process/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ input[type="checkbox"]:indeterminate::before {
/* List Table Header */
.table-header {
display: grid;
grid-template-columns: 36px minmax(180px, 3fr) 100px 140px 90px;
grid-template-columns: 36px minmax(140px, 3fr) 75px 130px 90px 65px;
padding: 10px 16px;
background-color: var(--bg-header);
border-bottom: 1px solid var(--border-color);
Expand Down Expand Up @@ -504,7 +504,7 @@ input[type="checkbox"]:indeterminate::before {

.process-item {
display: grid;
grid-template-columns: 36px minmax(180px, 3fr) 100px 140px 90px;
grid-template-columns: 36px minmax(140px, 3fr) 75px 130px 90px 65px;
align-items: center;
padding: 9px 12px;
margin-bottom: 4px;
Expand Down Expand Up @@ -556,6 +556,49 @@ input[type="checkbox"]:indeterminate::before {
font-size: 12px;
}

.port-cell {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: nowrap;
overflow: hidden;
}

.port-badge {
background-color: rgba(16, 185, 129, 0.12);
color: var(--accent-green);
border: 1px solid rgba(16, 185, 129, 0.25);
border-radius: var(--radius-sm);
padding: 1px 5px;
font-family: var(--font-mono);
font-size: 11px;
font-weight: 600;
white-space: nowrap;
}

.port-badge-active {
background-color: rgba(59, 130, 246, 0.12);
color: var(--accent-blue);
border-color: rgba(59, 130, 246, 0.25);
}

.port-badge-more {
background-color: var(--bg-surface-hover);
color: var(--text-muted);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 1px 4px;
font-family: var(--font-mono);
font-size: 10px;
}

.no-port {
color: var(--text-muted);
font-family: var(--font-mono);
font-size: 12px;
opacity: 0.5;
}

.mem-cell {
font-family: var(--font-mono);
font-weight: 600;
Expand Down
6 changes: 5 additions & 1 deletion plugins/ztools-kill-process/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ <h1 class="brand-title">进程管理与结束工具</h1>
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input type="text" id="searchInput" class="search-input" placeholder="搜索进程名称 / PID... (按 Esc 清空)" autocomplete="off" autofocus>
<input type="text" id="searchInput" class="search-input" placeholder="搜索进程名称 / PID / 占用端口 (如 8080 或 :3000)... (按 Esc 清空)" autocomplete="off" autofocus>
<div id="searchClear" class="search-clear" title="清空搜索">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
Expand Down Expand Up @@ -70,6 +70,10 @@ <h1 class="brand-title">进程管理与结束工具</h1>
<span>PID</span>
<span class="sort-icon"></span>
</div>
<div class="table-header-cell sortable" id="thPort">
<span>占用端口</span>
<span class="sort-icon"></span>
</div>
<div class="table-header-cell sortable" id="thMem">
<span>内存占用</span>
<span class="sort-icon"></span>
Expand Down
2 changes: 1 addition & 1 deletion plugins/ztools-kill-process/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"title": "杀进程/进程管理",
"pluginName": "杀进程/进程管理",
"description": "显示当前系统的进程列表,快速搜索与一键结束指定进程",
"version": "1.0.0",
"version": "1.0.1",
"author": "zhanglei",
"main": "index.html",
"preload": "preload.js",
Expand Down
Loading
Loading