feat(ui): 支持值得注意的Key多选、全选与批量操作

为“值得注意的Key”列表新增可见复选框和全选开关,提供
批量验证/复制/删除操作,并优化选择逻辑与样式。

- attentionKeysList 每行新增可见复选框并设置 data-key
- 新增“全选”和批量操作栏,实时显示已选数量
- getSelectedKeys/updateBatchActions/toggleSelectAll
  适配 attention 根节点,且仅作用于可见项
- initializeKeySelectionListeners 增加 attention 事件绑定
- fetchAndRenderAttentionKeys 阻止按钮冒泡、绑定复选框变更,
  在加载成功/失败后刷新批量栏状态
- attention 列表不与 valid/invalid 主列表同步勾选,避免交叉影响
- CSS 仅隐藏有效/无效列表复选框,新增 attention 列表
  hover/选中态样式
- 增强空值判断,避免批量栏或全选元素缺失时报错
This commit is contained in:
snaily
2025-08-18 16:31:48 +08:00
parent fa6745454e
commit 669123f348
2 changed files with 143 additions and 59 deletions

View File

@@ -108,8 +108,14 @@ function initStatItemAnimations() {
// 获取指定类型区域内选中的密钥
function getSelectedKeys(type) {
let selectorRoot;
if (type === 'attention') {
selectorRoot = '#attentionKeysList';
} else {
selectorRoot = `#${type}Keys`;
}
const checkboxes = document.querySelectorAll(
`#${type}Keys .key-checkbox:checked`
`${selectorRoot} .key-checkbox:checked`
);
return Array.from(checkboxes).map((cb) => cb.value);
}
@@ -119,27 +125,27 @@ function updateBatchActions(type) {
const selectedKeys = getSelectedKeys(type);
const count = selectedKeys.length;
const batchActionsDiv = document.getElementById(`${type}BatchActions`);
if (!batchActionsDiv) return;
const selectedCountSpan = document.getElementById(`${type}SelectedCount`);
const buttons = batchActionsDiv.querySelectorAll("button");
if (count > 0) {
batchActionsDiv.classList.remove("hidden");
selectedCountSpan.textContent = count;
if (selectedCountSpan) selectedCountSpan.textContent = count;
buttons.forEach((button) => (button.disabled = false));
} else {
batchActionsDiv.classList.add("hidden");
selectedCountSpan.textContent = "0";
if (selectedCountSpan) selectedCountSpan.textContent = "0";
buttons.forEach((button) => (button.disabled = true));
}
// 更新全选复选框状态
const selectAllCheckbox = document.getElementById(
`selectAll${type.charAt(0).toUpperCase() + type.slice(1)}`
);
const allCheckboxes = document.querySelectorAll(`#${type}Keys .key-checkbox`);
const selectAllId = `selectAll${type.charAt(0).toUpperCase() + type.slice(1)}`;
const selectAllCheckbox = document.getElementById(selectAllId);
const rootId = type === 'attention' ? 'attentionKeysList' : `${type}Keys`;
// 只有在有可见的 key 时才考虑全选状态
const visibleCheckboxes = document.querySelectorAll(
`#${type}Keys li:not([style*="display: none"]) .key-checkbox`
`#${rootId} li:not([style*="display: none"]) .key-checkbox`
);
if (selectAllCheckbox && visibleCheckboxes.length > 0) {
selectAllCheckbox.checked = count === visibleCheckboxes.length;
@@ -153,29 +159,28 @@ function updateBatchActions(type) {
// 全选/取消全选指定类型的密钥
function toggleSelectAll(type, isChecked) {
const listElement = document.getElementById(`${type}Keys`);
// Select checkboxes within LI elements that are NOT styled with display:none
// This targets currently visible items based on filtering.
const rootId = type === 'attention' ? 'attentionKeysList' : `${type}Keys`;
const listElement = document.getElementById(rootId);
if (!listElement) return;
const visibleCheckboxes = listElement.querySelectorAll(
`li:not([style*="display: none"]) .key-checkbox`
);
visibleCheckboxes.forEach((checkbox) => {
checkbox.checked = isChecked;
const listItem = checkbox.closest("li[data-key]"); // Get the LI from the current DOM
const listItem = checkbox.closest("li[data-key]");
if (listItem) {
listItem.classList.toggle("selected", isChecked);
// Sync with master array
const key = listItem.dataset.key;
const masterList = type === "valid" ? allValidKeys : allInvalidKeys;
if (masterList) {
// Ensure masterList is defined
const masterListItem = masterList.find((li) => li.dataset.key === key);
if (masterListItem) {
const masterCheckbox = masterListItem.querySelector(".key-checkbox");
if (masterCheckbox) {
masterCheckbox.checked = isChecked;
if (type !== 'attention') {
const key = listItem.dataset.key;
const masterList = type === "valid" ? allValidKeys : allInvalidKeys;
if (masterList) {
const masterListItem = masterList.find((li) => li.dataset.key === key);
if (masterListItem) {
const masterCheckbox = masterListItem.querySelector(".key-checkbox");
if (masterCheckbox) {
masterCheckbox.checked = isChecked;
}
}
}
}
@@ -1162,20 +1167,21 @@ function initializeKeySelectionListeners() {
if (listItem) {
listItem.classList.toggle("selected", checkbox.checked);
// Sync with master array
const key = listItem.dataset.key;
const masterList =
keyType === "valid" ? allValidKeys : allInvalidKeys;
if (masterList) {
// Ensure masterList is defined
const masterListItem = masterList.find(
(li) => li.dataset.key === key
);
if (masterListItem) {
const masterCheckbox =
masterListItem.querySelector(".key-checkbox");
if (masterCheckbox) {
masterCheckbox.checked = checkbox.checked;
// Sync with master array (only for valid/invalid lists)
if (keyType !== 'attention') {
const key = listItem.dataset.key;
const masterList =
keyType === "valid" ? allValidKeys : allInvalidKeys;
if (masterList) {
const masterListItem = masterList.find(
(li) => li.dataset.key === key
);
if (masterListItem) {
const masterCheckbox =
masterListItem.querySelector(".key-checkbox");
if (masterCheckbox) {
masterCheckbox.checked = checkbox.checked;
}
}
}
}
@@ -1187,6 +1193,7 @@ function initializeKeySelectionListeners() {
setupEventListenersForList("validKeys", "valid");
setupEventListenersForList("invalidKeys", "invalid");
setupEventListenersForList("attentionKeysList", "attention");
}
@@ -1579,33 +1586,43 @@ async function fetchAndRenderAttentionKeys(statusCode = 429, limit = 10) {
listEl.innerHTML = '';
if (!data || (Array.isArray(data) && data.length === 0) || data.error) {
listEl.innerHTML = '<li class="text-center text-gray-500 py-2">暂无需要注意的Key</li>';
updateBatchActions('attention');
return;
}
data.forEach(item => {
const li = document.createElement('li');
li.className = 'flex items-center justify-between bg-white rounded border px-3 py-2';
li.dataset.key = item.key || '';
const masked = item.key ? `${item.key.substring(0,4)}...${item.key.substring(item.key.length-4)}` : 'N/A';
const code = item.status_code ?? statusCode;
li.innerHTML = `
\u003cdiv class=\"flex items-center gap-3\"\u003e
\u003cspan class=\"font-mono text-sm\"\u003e${masked}\u003c/span\u003e
\u003cspan class=\"text-xs bg-red-100 text-red-700 px-2 py-0.5 rounded\"\u003e${code}: ${item.count}\u003c/span\u003e
\u003c/div\u003e
\u003cdiv class=\"flex items-center gap-2\"\u003e
\u003cbutton class=\"px-2 py-1 text-xs rounded bg-success-600 hover:bg-success-700 text-white\" title=\"验证此Key\"\u003e验证\u003c/button\u003e
\u003cbutton class=\"px-2 py-1 text-xs rounded bg-blue-600 hover:bg-blue-700 text-white\" title=\"查看24小时详情\"\u003e详情\u003c/button\u003e
\u003cbutton class=\"px-2 py-1 text-xs rounded bg-blue-500 hover:bg-blue-600 text-white\" title=\"复制Key\"\u003e复制\u003c/button\u003e
\u003cbutton class=\"px-2 py-1 text-xs rounded bg-red-800 hover:bg-red-900 text-white\" title=\"删除此Key\"\u003e删除\u003c/button\u003e
\u003c/div\u003e`;
<div class="flex items-center gap-3">
<input type="checkbox" class="form-checkbox h-4 w-4 text-primary-600 border-gray-300 rounded key-checkbox" value="${item.key || ''}">
<span class="font-mono text-sm">${masked}</span>
<span class="text-xs bg-red-100 text-red-700 px-2 py-0.5 rounded">${code}: ${item.count}</span>
</div>
<div class="flex items-center gap-2">
<button class="px-2 py-1 text-xs rounded bg-success-600 hover:bg-success-700 text-white" title="验证此Key">验证</button>
<button class="px-2 py-1 text-xs rounded bg-blue-600 hover:bg-blue-700 text-white" title="查看24小时详情">详情</button>
<button class="px-2 py-1 text-xs rounded bg-blue-500 hover:bg-blue-600 text-white" title="复制Key">复制</button>
<button class="px-2 py-1 text-xs rounded bg-red-800 hover:bg-red-900 text-white" title="删除此Key">删除</button>
</div>`;
const [verifyBtn, detailBtn, copyBtn, deleteBtn] = li.querySelectorAll('button');
verifyBtn.addEventListener('click', (e) => verifyKey(item.key, e.currentTarget));
detailBtn.addEventListener('click', () => window.showKeyUsageDetails(item.key));
copyBtn.addEventListener('click', () => copyKey(item.key));
deleteBtn.addEventListener('click', (e) => showSingleKeyDeleteConfirmModal(item.key, e.currentTarget));
verifyBtn.addEventListener('click', (e) => { e.stopPropagation(); verifyKey(item.key, e.currentTarget); });
detailBtn.addEventListener('click', (e) => { e.stopPropagation(); window.showKeyUsageDetails(item.key); });
copyBtn.addEventListener('click', (e) => { e.stopPropagation(); copyKey(item.key); });
deleteBtn.addEventListener('click', (e) => { e.stopPropagation(); showSingleKeyDeleteConfirmModal(item.key, e.currentTarget); });
// Checkbox change updates batch actions
const checkbox = li.querySelector('.key-checkbox');
if (checkbox) {
checkbox.addEventListener('change', () => updateBatchActions('attention'));
}
listEl.appendChild(li);
});
updateBatchActions('attention');
} catch (e) {
listEl.innerHTML = `<li class="text-center text-red-500 py-2">加载失败: ${e.message}</li>`;
updateBatchActions('attention');
}
}

View File

@@ -322,12 +322,13 @@ endblock %} {% block head_extra_styles %}
border-color: rgba(59, 130, 246, 0.3);
}
/* 隐藏原生复选框 */
.key-checkbox {
/* 隐藏原生复选框(仅隐藏有效/无效列表中的复选框保留值得注意的Key列表中的复选框可见 */
#validKeys .key-checkbox,
#invalidKeys .key-checkbox {
display: none;
}
/* 自定义复选框样式 */
/* 自定义复选框样式(仅针对有效/无效列表) */
#validKeys li::before,
#invalidKeys li::before {
content: "";
@@ -363,6 +364,31 @@ endblock %} {% block head_extra_styles %}
font-size: 0.8rem;
}
/* 值得注意的Key列表样式与选中态保留原生复选框可见 */
#attentionKeysList li {
display: flex;
align-items: center;
justify-content: space-between;
background-color: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 0.5rem;
padding: 0.5rem 0.75rem;
transition: all 0.2s ease;
cursor: pointer;
}
#attentionKeysList li:hover {
border-color: rgba(0, 0, 0, 0.12);
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
background-color: rgba(249, 250, 251, 0.95);
}
#attentionKeysList li.selected {
background-color: rgba(239, 246, 255, 0.95); /* light blue */
border-color: rgba(59, 130, 246, 0.35);
}
#attentionKeysList .key-checkbox {
margin-right: 0.25rem;
}
.key-text {
color: #374151 !important; /* gray-700 for light theme */
text-shadow: none;
@@ -1293,18 +1319,59 @@ endblock %} {% block head_extra_styles %}
<input id="attentionErrCustom" type="number" min="100" max="599" placeholder="自定义" class="form-input h-7 w-20 px-2 py-1 text-xs border rounded focus:ring-primary-500 focus:border-primary-500" />
<button id="attentionErrGo" class="px-2 py-1 rounded bg-blue-500 hover:bg-blue-600 text-white" title="查询">查询</button>
</div>
<div class="flex items-center gap-1 ml-3">
<div class="flex items-center gap-2 ml-3">
<label for="attentionLimitInput" class="text-xs text-gray-600">数量</label>
<input id="attentionLimitInput" type="number" min="1" max="1000" value="10" class="form-input h-7 w-20 px-2 py-1 text-xs border rounded focus:ring-primary-500 focus:border-primary-500" />
<!-- 全选移动到数量输入框右侧 -->
<div class="flex items-center gap-1">
<input
type="checkbox"
id="selectAllAttention"
class="form-checkbox h-4 w-4 text-primary-600 border-gray-300 rounded focus:ring-primary-500"
onchange="toggleSelectAll('attention', this.checked)"
/>
<label for="selectAllAttention" class="text-xs select-none whitespace-nowrap font-semibold" style="color: #1f2937 !important;">全选</label>
</div>
</div>
</div>
</div>
<div class="p-4">
<ul id="attentionKeysList" class="space-y-2">
<li class="text-center text-gray-500 py-2">加载中...</li>
</ul>
<div class="p-4">
<!-- 批量操作按钮组 (仅在选中时显示) -->
<div
id="attentionBatchActions"
class="p-3 border mb-3 hidden flex items-center flex-wrap gap-3"
style="background-color: rgba(249, 250, 251, 0.95); border-color: rgba(0, 0, 0, 0.08);"
>
<span class="text-sm font-semibold whitespace-nowrap" style="color: #1f2937 !important;">
已选择 <span id="attentionSelectedCount">0</span>
</span>
<button
class="flex items-center gap-1 bg-success-600 hover:bg-success-700 text-white px-2.5 py-1 rounded-lg text-xs font-medium transition-all duration-200 disabled:cursor-not-allowed"
onclick="event.stopPropagation(); showVerifyModal('attention', event)"
disabled
>
<i class="fas fa-check-double"></i> 批量验证
</button>
<button
class="flex items-center gap-1 bg-blue-500 hover:bg-blue-600 text-white px-2.5 py-1 rounded-lg text-xs font-medium transition-all duration-200 disabled:cursor-not-allowed"
onclick="event.stopPropagation(); copySelectedKeys('attention')"
disabled
>
<i class="fas fa-copy"></i> 批量复制
</button>
<button
class="flex items-center gap-1 bg-red-800 hover:bg-red-900 text-white px-2.5 py-1 rounded-lg text-xs font-medium transition-all duration-200 disabled:cursor-not-allowed"
onclick="event.stopPropagation(); showDeleteConfirmationModal('attention', event)"
disabled
>
<i class="fas fa-trash-alt"></i> 批量删除
</button>
</div>
<ul id="attentionKeysList" class="space-y-2">
<li class="text-center text-gray-500 py-2">加载中...</li>
</ul>
</div>
</div>
</div>
<!-- 有效密钥区域 -->