<!DOCTYPE html>
<html lang=
"zh-CN"
>
<head>
<meta charset=
"UTF-8"
>
<meta
name
=
"viewport"
content=
"width=device-width, initial-scale=1.0"
>
<
title
>书签链接检测工具</
title
>
<style>
:root {
--primary-color: #4285f4;
--success-color: #34a853;
--danger-color: #ea4335;
--warning-color: #fbbc05;
--light-color: #f8f9fa;
--dark-color: #343a40;
}
body {
font-family:
'Segoe UI'
, Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f5f5f5;
margin: 0;
padding: 0;
}
.container {
max-
width
: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
background-color: var(--primary-color);
color: white;
padding: 20px 0;
margin-bottom: 30px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
header h1 {
margin: 0;
text-
align
: center;
font-weight: 500;
}
.card {
background-color: white;
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
padding: 20px;
margin-bottom: 20px;
}
.card-
title
{
margin-top: 0;
color: var(--primary-color);
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.form-
group
{
margin-bottom: 15px;
}
label
{
display: block;
margin-bottom: 5px;
font-weight: 500;
}
input[
type
=
"number"
], select {
width
: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.btn {
display: inline-block;
background-color: var(--primary-color);
color: white;
padding: 10px 15px;
border:
none
;
border-radius: 4px;
cursor: pointer;
font-
size
: 16px;
transition: background-color 0.3s;
}
.btn:hover {
background-color: #3367d6;
}
.btn-success {
background-color: var(--success-color);
}
.btn-success:hover {
background-color: #2d9249;
}
.btn-danger {
background-color: var(--danger-color);
}
.btn-danger:hover {
background-color: #d33426;
}
.btn-warning {
background-color: var(--warning-color);
}
.btn-warning:hover {
background-color: #e6ac00;
}
.progress-container {
margin-top: 20px;
background-color: #eee;
border-radius: 5px;
height: 20px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background-color: var(--primary-color);
width
: 0%;
transition:
width
0.3s;
}
.stats {
display: flex;
justify-content: space-between;
margin-top: 15px;
font-
size
: 14px;
}
.stat-item {
text-
align
: center;
flex: 1;
}
.stat-value {
font-
size
: 18px;
font-weight: bold;
}
.bookmark-list {
margin-top: 20px;
border: 1px solid #ddd;
border-radius: 5px;
overflow: hidden;
}
.bookmark-header {
display: flex;
background-color: #f8f9fa;
padding: 10px 15px;
font-weight: bold;
border-bottom: 1px solid #ddd;
}
.bookmark-header
div
{
flex: 1;
}
.bookmark-header .checkbox {
flex: 0 0 40px;
}
.bookmark-header .status {
flex: 0 0 100px;
}
.bookmark-header .actions {
flex: 0 0 150px;
}
.bookmark-item {
display: flex;
padding: 10px 15px;
border-bottom: 1px solid #eee;
align
-items: center;
}
.bookmark-item:last-child {
border-bottom:
none
;
}
.bookmark-item:hover {
background-color: #f8f9fa;
}
.bookmark-item
div
{
flex: 1;
word
-break: break-
all
;
}
.bookmark-item .checkbox {
flex: 0 0 40px;
}
.bookmark-item .status {
flex: 0 0 100px;
}
.bookmark-item .actions {
flex: 0 0 150px;
}
.status-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 12px;
font-
size
: 12px;
font-weight: bold;
}
.status-valid {
background-color: #e6f4ea;
color: var(--success-color);
}
.status-invalid {
background-color: #fce8e6;
color: var(--danger-color);
}
.status-timeout {
background-color: #fef7e0;
color: var(--warning-color);
}
.status-pending {
background-color: #e8f0fe;
color: var(--primary-color);
}
.action-btn {
background:
none
;
border:
none
;
color: var(--primary-color);
cursor: pointer;
margin-right: 5px;
font-
size
: 14px;
}
.action-btn:hover {
text-decoration: underline;
}
.footer {
text-
align
: center;
margin-top: 30px;
color: #777;
font-
size
: 14px;
}
.drag-drop-area {
border: 2px dashed #ccc;
border-radius: 5px;
padding: 30px;
text-
align
: center;
margin-bottom: 20px;
cursor: pointer;
transition: background-color 0.3s;
}
.drag-drop-area:hover {
background-color: #f8f9fa;
}
.drag-drop-area.active {
border-color: var(--primary-color);
background-color: #e8f0fe;
}
.tab-container {
margin-bottom: 20px;
}
.tab-buttons {
display: flex;
border-bottom: 1px solid #ddd;
}
.tab-button {
padding: 10px 20px;
background:
none
;
border:
none
;
cursor: pointer;
font-
size
: 16px;
color: #666;
position: relative;
}
.tab-button.active {
color: var(--primary-color);
font-weight: bold;
}
.tab-button.active::after {
content:
''
;
position:
absolute
;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background-color: var(--primary-color);
}
.tab-content {
display:
none
;
padding: 20px 0;
}
.tab-content.active {
display: block;
}
.checkbox {
text-
align
: center;
}
.select-
all
{
margin-right: 10px;
}
@media (max-
width
: 768px) {
.bookmark-header, .bookmark-item {
flex-wrap: wrap;
}
.bookmark-header
div
, .bookmark-item
div
{
flex: 0 0 100%;
margin-bottom: 5px;
}
.bookmark-header .checkbox, .bookmark-item .checkbox {
flex: 0 0 30px;
}
.bookmark-header .status, .bookmark-item .status {
flex: 0 0 80px;
}
.bookmark-header .actions, .bookmark-item .actions {
flex: 0 0 100%;
text-
align
: right;
}
}
</style>
</head>
<body>
<
div
class=
"container"
>
<header>
<h1>书签链接检测工具</h1>
</header>
<
div
class=
"card"
>
<h2 class=
"card-title"
>导入书签</h2>
<
div
class=
"drag-drop-area"
id=
"dragDropArea"
>
<p>拖放浏览器导出的HTML书签文件到这里</p>
<p>或</p>
<input
type
=
"file"
id=
"fileInput"
accept=
".html"
style=
"display: none;"
>
<button class=
"btn"
id=
"selectFileBtn"
>选择文件</button>
</
div
>
</
div
>
<
div
class=
"card"
>
<h2 class=
"card-title"
>检测设置</h2>
<
div
class=
"form-group"
>
<
label
for
=
"timeout"
>超时时间 (毫秒)</
label
>
<input
type
=
"number"
id=
"timeout"
min=
"500"
max=
"10000"
value=
"3000"
>
</
div
>
<
div
class=
"form-group"
>
<
label
for
=
"retryCount"
>每个链接检测次数 (1-5次)</
label
>
<input
type
=
"number"
id=
"retryCount"
min=
"1"
max=
"5"
value=
"3"
>
</
div
>
<
div
class=
"form-group"
>
<
label
for
=
"concurrency"
>并发请求数 (1-10个)</
label
>
<input
type
=
"number"
id=
"concurrency"
min=
"1"
max=
"10"
value=
"5"
>
</
div
>
<button class=
"btn btn-success"
id=
"startCheckBtn"
>开始检测</button>
<button class=
"btn btn-danger"
id=
"stopCheckBtn"
disabled>停止检测</button>
<
div
class=
"progress-container"
id=
"progressContainer"
style=
"display: none;"
>
<
div
class=
"progress-bar"
id=
"progressBar"
></
div
>
</
div
>
<
div
class=
"stats"
id=
"stats"
style=
"display: none;"
>
<
div
class=
"stat-item"
>
<
div
class=
"stat-label"
>总数</
div
>
<
div
class=
"stat-value"
id=
"totalCount"
>0</
div
>
</
div
>
<
div
class=
"stat-item"
>
<
div
class=
"stat-label"
>已完成</
div
>
<
div
class=
"stat-value"
id=
"completedCount"
>0</
div
>
</
div
>
<
div
class=
"stat-item"
>
<
div
class=
"stat-label"
>有效</
div
>
<
div
class=
"stat-value"
id=
"validCount"
>0</
div
>
</
div
>
<
div
class=
"stat-item"
>
<
div
class=
"stat-label"
>无效</
div
>
<
div
class=
"stat-value"
id=
"invalidCount"
>0</
div
>
</
div
>
<
div
class=
"stat-item"
>
<
div
class=
"stat-label"
>超时</
div
>
<
div
class=
"stat-value"
id=
"timeoutCount"
>0</
div
>
</
div
>
</
div
>
</
div
>
<
div
class=
"card"
>
<
div
class=
"tab-container"
>
<
div
class=
"tab-buttons"
>
<button class=
"tab-button active"
data-tab=
"all"
>全部书签</button>
<button class=
"tab-button"
data-tab=
"valid"
>有效链接</button>
<button class=
"tab-button"
data-tab=
"invalid"
>无效链接</button>
<button class=
"tab-button"
data-tab=
"timeout"
>超时链接</button>
</
div
>
<
div
class=
"tab-content active"
id=
"tab-all"
>
<
div
class=
"bookmark-actions"
>
<input
type
=
"checkbox"
id=
"selectAll"
class=
"select-all"
>
<
label
for
=
"selectAll"
>全选</
label
>
<button class=
"btn btn-warning"
id=
"deleteInvalidBtn"
>删除无效链接</button>
<button class=
"btn"
id=
"exportSelectedBtn"
>导出选中</button>
<select id=
"exportFilter"
>
<
option
value=
"all"
>全部</
option
>
<
option
value=
"valid"
>仅有效</
option
>
<
option
value=
"invalid"
>仅无效</
option
>
<
option
value=
"timeout"
>仅超时</
option
>
</select>
</
div
>
<
div
class=
"bookmark-list"
>
<
div
class=
"bookmark-header"
>
<
div
class=
"checkbox"
></
div
>
<
div
>标题</
div
>
<
div
>URL</
div
>
<
div
class=
"status"
>状态</
div
>
<
div
class=
"actions"
>操作</
div
>
</
div
>
<
div
id=
"bookmarkList"
>
<!-- 书签列表将在这里动态生成 -->
<
div
class=
"empty-message"
>请先导入书签文件</
div
>
</
div
>
</
div
>
</
div
>
<
div
class=
"tab-content"
id=
"tab-valid"
>
<
div
class=
"bookmark-list"
>
<
div
class=
"bookmark-header"
>
<
div
class=
"checkbox"
></
div
>
<
div
>标题</
div
>
<
div
>URL</
div
>
<
div
class=
"status"
>状态</
div
>
<
div
class=
"actions"
>操作</
div
>
</
div
>
<
div
id=
"validBookmarkList"
>
<!-- 有效书签列表将在这里动态生成 -->
</
div
>
</
div
>
</
div
>
<
div
class=
"tab-content"
id=
"tab-invalid"
>
<
div
class=
"bookmark-list"
>
<
div
class=
"bookmark-header"
>
<
div
class=
"checkbox"
></
div
>
<
div
>标题</
div
>
<
div
>URL</
div
>
<
div
class=
"status"
>状态</
div
>
<
div
class=
"actions"
>操作</
div
>
</
div
>
<
div
id=
"invalidBookmarkList"
>
<!-- 无效书签列表将在这里动态生成 -->
</
div
>
</
div
>
</
div
>
<
div
class=
"tab-content"
id=
"tab-timeout"
>
<
div
class=
"bookmark-list"
>
<
div
class=
"bookmark-header"
>
<
div
class=
"checkbox"
></
div
>
<
div
>标题</
div
>
<
div
>URL</
div
>
<
div
class=
"status"
>状态</
div
>
<
div
class=
"actions"
>操作</
div
>
</
div
>
<
div
id=
"timeoutBookmarkList"
>
<!-- 超时书签列表将在这里动态生成 -->
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
<
div
class=
"footer"
>
<p>书签链接检测工具 © 2023 | 使用HTTP HEAD方法检测链接有效性</p>
</
div
>
</
div
>
<script>
document.addEventListener(
'DOMContentLoaded'
, function() {
// 全局变量
let bookmarks = [];
let isChecking = false;
let stopRequested = false;
let currentTab =
'all'
;
// DOM元素
const dragDropArea = document.getElementById(
'dragDropArea'
);
const fileInput = document.getElementById(
'fileInput'
);
const selectFileBtn = document.getElementById(
'selectFileBtn'
);
const startCheckBtn = document.getElementById(
'startCheckBtn'
);
const stopCheckBtn = document.getElementById(
'stopCheckBtn'
);
const progressBar = document.getElementById(
'progressBar'
);
const progressContainer = document.getElementById(
'progressContainer'
);
const stats = document.getElementById(
'stats'
);
const bookmarkList = document.getElementById(
'bookmarkList'
);
const validBookmarkList = document.getElementById(
'validBookmarkList'
);
const invalidBookmarkList = document.getElementById(
'invalidBookmarkList'
);
const timeoutBookmarkList = document.getElementById(
'timeoutBookmarkList'
);
const totalCountEl = document.getElementById(
'totalCount'
);
const completedCountEl = document.getElementById(
'completedCount'
);
const validCountEl = document.getElementById(
'validCount'
);
const invalidCountEl = document.getElementById(
'invalidCount'
);
const timeoutCountEl = document.getElementById(
'timeoutCount'
);
const selectAllCheckbox = document.getElementById(
'selectAll'
);
const deleteInvalidBtn = document.getElementById(
'deleteInvalidBtn'
);
const exportSelectedBtn = document.getElementById(
'exportSelectedBtn'
);
const exportFilter = document.getElementById(
'exportFilter'
);
const tabButtons = document.querySelectorAll(
'.tab-button'
);
const tabContents = document.querySelectorAll(
'.tab-content'
);
// 事件监听器
selectFileBtn.addEventListener(
'click'
, function() {
fileInput.click();
});
fileInput.addEventListener(
'change'
, handleFileSelect);
dragDropArea.addEventListener(
'dragover'
, function(e) {
e.preventDefault();
dragDropArea.classList.
add
(
'active'
);
});
dragDropArea.addEventListener(
'dragleave'
, function() {
dragDropArea.classList.remove(
'active'
);
});
dragDropArea.addEventListener(
'drop'
, function(e) {
e.preventDefault();
dragDropArea.classList.remove(
'active'
);
if
(e.dataTransfer.files.
length
) {
fileInput.files = e.dataTransfer.files;
handleFileSelect({ target: fileInput });
}
});
startCheckBtn.addEventListener(
'click'
, startChecking);
stopCheckBtn.addEventListener(
'click'
, stopChecking);
selectAllCheckbox.addEventListener(
'change'
, function() {
const checkboxes = document.querySelectorAll(
'.bookmark-checkbox'
);
checkboxes.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
});
});
deleteInvalidBtn.addEventListener(
'click'
, deleteInvalidBookmarks);
exportSelectedBtn.addEventListener(
'click'
, exportSelectedBookmarks);
tabButtons.forEach(button => {
button.addEventListener(
'click'
, function() {
const tabId =
this
.getAttribute(
'data-tab'
);
switchTab(tabId);
});
});
// 函数定义
function handleFileSelect(event) {
const file = event.target.files[0];
if
(!file) return;
const reader = new FileReader();
reader.onload = function(e) {
try {
const content = e.target.result;
bookmarks = parseBookmarks(content);
renderBookmarkList();
updateStats();
// 重置状态
resetCheckingState();
} catch (
error
) {
alert(
'解析书签文件失败: '
+
error
.message);
}
};
reader.readAsText(file);
}
function parseBookmarks(html) {
const parser = new DOMParser();
const doc = parser.parseFromString(html,
'text/html'
);
const links = doc.querySelectorAll(
'a'
);
return Array.from(links).map(link => {
return {
title
: link.textContent.trim(),
url: link.getAttribute(
'href'
),
status:
'pending'
,
retries: 0,
validCount: 0,
invalidCount: 0,
timeoutCount: 0,
selected: false
};
});
}
function renderBookmarkList() {
if
(bookmarks.
length
=== 0) {
bookmarkList.innerHTML =
'<div class="empty-message">请先导入书签文件</div>'
;
return;
}
bookmarkList.innerHTML =
''
;
validBookmarkList.innerHTML =
''
;
invalidBookmarkList.innerHTML =
''
;
timeoutBookmarkList.innerHTML =
''
;
bookmarks.forEach((bookmark, index) => {
const bookmarkItem = createBookmarkElement(bookmark, index);
bookmarkList.appendChild(bookmarkItem);
// 根据状态添加到对应的列表
if
(bookmark.status ===
'valid'
) {
validBookmarkList.appendChild(createBookmarkElement(bookmark, index));
}
else
if
(bookmark.status ===
'invalid'
) {
invalidBookmarkList.appendChild(createBookmarkElement(bookmark, index));
}
else
if
(bookmark.status ===
'timeout'
) {
timeoutBookmarkList.appendChild(createBookmarkElement(bookmark, index));
}
});
}
function createBookmarkElement(bookmark, index) {
const item = document.createElement(
'div'
);
item.className =
'bookmark-item'
;
// 状态徽章
let statusBadge =
''
;
let statusText =
''
;
let statusClass =
''
;
if
(bookmark.status ===
'valid'
) {
statusText =
'有效'
;
statusClass =
'status-valid'
;
}
else
if
(bookmark.status ===
'invalid'
) {
statusText =
'无效'
;
statusClass =
'status-invalid'
;
}
else
if
(bookmark.status ===
'timeout'
) {
statusText =
'超时'
;
statusClass =
'status-timeout'
;
}
else
{
statusText =
'待检测'
;
statusClass =
'status-pending'
;
}
// 检测统计
let statsText =
''
;
if
(bookmark.retries > 0) {
statsText = `${bookmark.validCount}√/${bookmark.invalidCount}×/${bookmark.timeoutCount}⌛`;
}
item.innerHTML = `
<
div
class=
"checkbox"
>
<input
type
=
"checkbox"
class=
"bookmark-checkbox"
data-index=
"${index}"
${bookmark.selected ?
'checked'
:
''
}>
</
div
>
<
div
>${bookmark.
title
||
'无标题'
}</
div
>
<
div
>${bookmark.url}</
div
>
<
div
class=
"status"
>
<span class=
"status-badge ${statusClass}"
>${statusText}</span>
${statsText ? `<br><
small
>${statsText}</
small
>` :
''
}
</
div
>
<
div
class=
"actions"
>
<button class=
"action-btn test-btn"
data-index=
"${index}"
>测试</button>
<button class=
"action-btn visit-btn"
data-index=
"${index}"
>访问</button>
</
div
>
`;
// 添加事件监听器
const checkbox = item.querySelector(
'.bookmark-checkbox'
);
checkbox.addEventListener(
'change'
, function() {
bookmarks[index].selected =
this
.checked;
updateSelectAllState();
});
const testBtn = item.querySelector(
'.test-btn'
);
testBtn.addEventListener(
'click'
, function() {
testSingleBookmark(index);
});
const visitBtn = item.querySelector(
'.visit-btn'
);
visitBtn.addEventListener(
'click'
, function() {
window.open(bookmark.url,
'_blank'
);
});
return item;
}
function startChecking() {
if
(bookmarks.
length
=== 0) {
alert(
'请先导入书签文件'
);
return;
}
if
(isChecking) {
alert(
'检测正在进行中'
);
return;
}
isChecking = true;
stopRequested = false;
startCheckBtn.disabled = true;
stopCheckBtn.disabled = false;
const timeout = parseInt(document.getElementById(
'timeout'
).value) || 3000;
const retryCount = parseInt(document.getElementById(
'retryCount'
).value) || 3;
const concurrency = parseInt(document.getElementById(
'concurrency'
).value) || 5;
progressContainer.style.display =
'block'
;
stats.style.display =
'flex'
;
// 重置所有书签状态
bookmarks.forEach(bookmark => {
bookmark.status =
'pending'
;
bookmark.retries = 0;
bookmark.validCount = 0;
bookmark.invalidCount = 0;
bookmark.timeoutCount = 0;
});
renderBookmarkList();
updateStats();
// 使用并发控制检测书签
const total = bookmarks.
length
;
let currentIndex = 0;
let activeRequests = 0;
function processNextBatch() {
if
(stopRequested || currentIndex >= total) {
if
(activeRequests === 0) {
finishChecking();
}
return;
}
const batchSize = Math.min(concurrency - activeRequests, total - currentIndex);
for
(let i = 0; i < batchSize; i++) {
if
(currentIndex < total) {
activeRequests++;
checkBookmark(currentIndex, timeout, retryCount)
.finally(() => {
activeRequests--;
processNextBatch();
});
currentIndex++;
}
}
}
processNextBatch();
}
async function checkBookmark(index, timeout, maxRetries) {
if
(stopRequested) return;
const bookmark = bookmarks[index];
bookmark.retries = 0;
bookmark.validCount = 0;
bookmark.invalidCount = 0;
bookmark.timeoutCount = 0;
for
(let i = 0; i < maxRetries; i++) {
if
(stopRequested) break;
bookmark.retries++;
try {
const isValid = await checkUrl(bookmark.url, timeout);
if
(isValid) {
bookmark.validCount++;
}
else
{
bookmark.invalidCount++;
}
} catch (
error
) {
if
(
error
.message ===
'Timeout'
) {
bookmark.timeoutCount++;
}
else
{
bookmark.invalidCount++;
}
}
// 更新UI
updateBookmarkStatus(index);
updateStats();
}
// 根据多数结果确定最终状态
const counts = [
{ status:
'valid'
, count: bookmark.validCount },
{ status:
'invalid'
, count: bookmark.invalidCount },
{ status:
'timeout'
, count: bookmark.timeoutCount }
];
counts.sort((a, b) => b.count - a.count);
bookmark.status = counts[0].count > 0 ? counts[0].status :
'invalid'
;
updateBookmarkStatus(index);
updateStats();
}
function checkUrl(url, timeout) {
return new Promise((resolve, reject) => {
if
(!url || !url.startsWith(
'http'
)) {
resolve(false);
return;
}
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
reject(new
Error
(
'Timeout'
));
}, timeout);
fetch(url, {
method:
'HEAD'
,
mode:
'no-cors'
,
signal: controller.signal
})
.then(response => {
clearTimeout(timeoutId);
// 在no-cors模式下,我们无法读取响应状态,所以假设它是有效的
resolve(true);
})
.catch(
error
=> {
clearTimeout(timeoutId);
if
(
error
.
name
===
'AbortError'
) {
reject(new
Error
(
'Timeout'
));
}
else
{
// 其他错误视为无效
resolve(false);
}
});
});
}
function stopChecking() {
stopRequested = true;
stopCheckBtn.disabled = true;
}
function finishChecking() {
isChecking = false;
startCheckBtn.disabled = false;
stopCheckBtn.disabled = true;
if
(!stopRequested) {
progressBar.style.
width
=
'100%'
;
}
// 重新渲染列表以更新分类
renderBookmarkList();
}
function updateBookmarkStatus(index) {
const bookmark = bookmarks[index];
const items = document.querySelectorAll(`.bookmark-checkbox[data-index=
"${index}"
]`);
items.forEach(item => {
const itemContainer = item.closest(
'.bookmark-item'
);
if
(itemContainer) {
const statusBadge = itemContainer.querySelector(
'.status-badge'
);
const statusText = itemContainer.querySelector(
'.status small'
);
if
(statusBadge) {
// 更新状态徽章
let statusClass =
''
;
let text =
''
;
if
(bookmark.status ===
'valid'
) {
text =
'有效'
;
statusClass =
'status-valid'
;
}
else
if
(bookmark.status ===
'invalid'
) {
text =
'无效'
;
statusClass =
'status-invalid'
;
}
else
if
(bookmark.status ===
'timeout'
) {
text =
'超时'
;
statusClass =
'status-timeout'
;
}
else
{
text =
'检测中'
;
statusClass =
'status-pending'
;
}
statusBadge.className = `status-badge ${statusClass}`;
statusBadge.textContent = text;
// 更新统计信息
if
(statusText) {
statusText.textContent = `${bookmark.validCount}√/${bookmark.invalidCount}×/${bookmark.timeoutCount}⌛`;
}
}
}
});
}
function updateStats() {
const total = bookmarks.
length
;
const completed = bookmarks.filter(b => b.retries > 0).
length
;
const valid = bookmarks.filter(b => b.status ===
'valid'
).
length
;
const invalid = bookmarks.filter(b => b.status ===
'invalid'
).
length
;
const timeout = bookmarks.filter(b => b.status ===
'timeout'
).
length
;
totalCountEl.textContent = total;
completedCountEl.textContent = completed;
validCountEl.textContent = valid;
invalidCountEl.textContent = invalid;
timeoutCountEl.textContent = timeout;
// 更新进度条
if
(total > 0) {
const progress = (completed / total) * 100;
progressBar.style.
width
= `${progress}%`;
}
}
function resetCheckingState() {
isChecking = false;
stopRequested = false;
startCheckBtn.disabled = false;
stopCheckBtn.disabled = true;
progressBar.style.
width
=
'0%'
;
progressContainer.style.display =
'none'
;
stats.style.display =
'none'
;
}
function testSingleBookmark(index) {
const timeout = parseInt(document.getElementById(
'timeout'
).value) || 3000;
const retryCount = parseInt(document.getElementById(
'retryCount'
).value) || 3;
bookmarks[index].status =
'pending'
;
bookmarks[index].retries = 0;
bookmarks[index].validCount = 0;
bookmarks[index].invalidCount = 0;
bookmarks[index].timeoutCount = 0;
updateBookmarkStatus(index);
checkBookmark(index, timeout, retryCount);
}
function deleteInvalidBookmarks() {
if
(confirm(
'确定要删除所有无效链接吗?'
)) {
bookmarks = bookmarks.filter(bookmark => bookmark.status !==
'invalid'
);
renderBookmarkList();
updateStats();
}
}
function exportSelectedBookmarks() {
const filter = exportFilter.value;
const selectedBookmarks = bookmarks.filter(bookmark => {
if
(!bookmark.selected) return false;
if
(filter ===
'valid'
) return bookmark.status ===
'valid'
;
if
(filter ===
'invalid'
) return bookmark.status ===
'invalid'
;
if
(filter ===
'timeout'
) return bookmark.status ===
'timeout'
;
return true;
});
if
(selectedBookmarks.
length
=== 0) {
alert(
'没有选中的书签'
);
return;
}
const html = generateBookmarkHTML(selectedBookmarks);
downloadHTML(html,
'bookmarks_export.html'
);
}
function generateBookmarkHTML(bookmarksToExport) {
let html = `<!DOCTYPE NETSCAPE-Bookmark-file-1>
<META HTTP-EQUIV=
"Content-Type"
CONTENT=
"text/html; charset=UTF-8"
>
<
TITLE
>Bookmarks</
TITLE
>
<H1>Bookmarks</H1>
<
DL
><p>
`;
bookmarksToExport.forEach(bookmark => {
html += ` <
DT
><A HREF=
"${bookmark.url}"
ADD_DATE=
"${Math.floor(Date.now()/1000)}"
>${bookmark.
title
|| bookmark.url}</A>\n`;
});
html += `</
DL
><p>`;
return html;
}
function downloadHTML(content, filename) {
const blob = new Blob([content], {
type
:
'text/html'
});
const url = URL.createObjectURL(blob);
const a = document.createElement(
'a'
);
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function updateSelectAllState() {
const selectedCount = bookmarks.filter(b => b.selected).
length
;
selectAllCheckbox.checked = selectedCount > 0 && selectedCount === bookmarks.
length
;
selectAllCheckbox.indeterminate = selectedCount > 0 && selectedCount < bookmarks.
length
;
}
function switchTab(tabId) {
// 更新按钮状态
tabButtons.forEach(button => {
button.classList.remove(
'active'
);
if
(button.getAttribute(
'data-tab'
) === tabId) {
button.classList.
add
(
'active'
);
}
});
// 更新内容区域
tabContents.forEach(content => {
content.classList.remove(
'active'
);
if
(content.id === `tab-${tabId}`) {
content.classList.
add
(
'active'
);
}
});
currentTab = tabId;
}
});
</script>
</body>
</html>