部署在CF的轻量化导航页面,可移动卡片式书签,方便管理
github项目
Card-Tab 书签卡片式管理,进入管理模式可以自由移动书签位置,添加和删除书签,支持自定义网站分类,支持切换黑暗色主题
一、cloudflare workes部署
新版本部署
2024.10.30 更新:
1)、增加了前端验证,并取消了在浏览器中保存日志。现在超过15分钟需重新登录,及时退出登录能让你的隐私更安全;
2)、进入设置之前会在自动备份书签,KV里将保存最近10次的备份;
3)、小幅更改了配色。
1、原workes,
const HTML_CONTENT = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Card Tab</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2280%22>⭐</text></svg>">
<style>
/* 全局样式 */
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #e8f4ea;
transition: background-color 0.3s ease;
}
/* 固定元素样式 */
.fixed-elements {
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: #e8f4ea;
z-index: 1000;
padding: 10px;
transition: background-color 0.3s ease;
height: 130px;
}
.fixed-elements h3 {
position: absolute;
top: 10px;
left: 20px;
margin: 0;
}
/* 中心内容样式 */
.center-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
max-width: 600px;
text-align: center;
}
/* 管理员控制面板样式 */
.admin-controls {
position: fixed;
top: 10px;
right: 10px;
font-size: 60%;
}
/* 添加/删除控制按钮样式 */
.add-remove-controls {
display: none;
flex-direction: column;
position: fixed;
right: 20px;
top: 50%;
transform: translateY(-50%);
align-items: center;
gap: 10px;
}
.round-btn {
background-color: #007bff;
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
text-align: center;
font-size: 24px;
line-height: 40px;
cursor: pointer;
margin: 5px 0;
}
.add-btn { order: 1; }
.remove-btn { order: 2; }
.category-btn { order: 3; }
.remove-category-btn { order: 4; }
/* 主要内容区域样式 */
.content {
margin-top: 140px;
padding: 20px;
}
/* 搜索栏样式 */
.search-container {
margin-top: 10px;
}
.search-bar {
display: flex;
justify-content: center;
margin-bottom: 10px;
}
.search-bar input {
width: 70%;
padding: 5px;
border: 1px solid #ccc;
border-radius: 5px 0 0 5px;
}
.search-bar button {
padding: 5px 10px;
border: 1px solid #ccc;
border-left: none;
background-color: #f8f8;
border-radius: 0 5px 5px 0;
cursor: pointer;
}
/* 搜索引擎按钮样式 */
.search-engines {
display: flex;
justify-content: center;
gap: 10px;
}
.search-engine {
padding: 5px 10px;
border: 1px solid #ccc;
background-color: #f0f0f0;
border-radius: 5px;
cursor: pointer;
}
/* 主题切换按钮样式 */
#theme-toggle {
position: fixed;
bottom: 50px;
right: 20px;
background-color: #b8c9d9;
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
text-align: center;
font-size: 24px;
line-height: 40px;
cursor: pointer;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
transition: background-color 0.3s ease;
}
#theme-toggle:hover {
background-color: #007bff;
}
/* 对话框样式 */
#dialog-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
}
#dialog-box {
background-color: white;
padding: 20px;
border-radius: 5px;
width: 300px;
}
#dialog-box input, #dialog-box select {
width: 100%;
margin-bottom: 10px;
padding: 5px;
}
/* 分类和卡片样式 */
.section {
margin-bottom: 20px;
}
.section-title-container {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.section-title {
font-size: 18px;
font-weight: bold;
}
.delete-category-btn {
background-color: #ff9800;
color: white;
border: none;
padding: 5px 10px;
border-radius: 5px;
cursor: pointer;
}
.card-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.card {
background-color: #b8c9d9;
border-radius: 5px;
padding: 10px;
width: 150px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s;
position: relative;
user-select: none;
}
.card:hover {
transform: translateY(-5px);
}
.card-top {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.card-icon {
width: 16px;
height: 16px;
margin-right: 5px;
}
.card-title {
font-size: 14px;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-url {
font-size: 12px;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.private-tag {
background-color: #ff9800;
color: white;
font-size: 10px;
padding: 2px 5px;
border-radius: 3px;
position: absolute;
top: 5px;
right: 5px;
}
.delete-btn {
position: absolute;
top: -10px;
right: -10px;
background-color: red;
color: white;
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
text-align: center;
font-size: 14px;
line-height: 20px;
cursor: pointer;
display: none;
}
/* 版权信息样式 */
#copyright {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 40px;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
z-index: 1000;
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
}
#copyright p {
margin: 0;
}
#copyright a {
color: #007bff;
text-decoration: none;
}
#copyright a:hover {
text-decoration: underline;
}
/* 响应式设计 */
@media (max-width: 480px) {
.fixed-elements {
position: relative;
padding: 5px;
}
.content {
margin-top: 10px;
}
.admin-controls input,
.admin-controls button {
height: 30%;
}
.card-container {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.card {
width: 80%;
max-width: 100%;
padding: 5px;
}
.card-title {
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 130px;
}
.card-url {
font-size: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 130px;
}
.add-remove-controls {
right: 2px;
}
.round-btn,
#theme-toggle {
right: 5px;
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
font-size: 24px;
}
}
</style>
</head>
<body>
<div class="fixed-elements">
<h3>我的导航</h3>
<div class="center-content">
<!-- 一言模块 -->
<p id="hitokoto">
<a href="#" id="hitokoto_text"></a>
</p>
<script src="https://v1.hitokoto.cn/?encode=js&select=%23hitokoto" defer></script>
<!-- 搜索栏 -->
<div class="search-container">
<div class="search-bar">
<input type="text" id="search-input" placeholder="">
<button id="search-button">🔍</button>
</div>
<div class="search-engines">
<button class="search-engine" data-engine="baidu">百度</button>
<button class="search-engine" data-engine="bing">必应</button>
<button class="search-engine" data-engine="google">谷歌</button>
</div>
</div>
</div>
<!-- 管理员控制面板 -->
<div class="admin-controls">
<input type="password" id="admin-password" placeholder="输入密码">
<button id="admin-mode-btn" onclick="toggleAdminMode()">设 置</button>
<button id="secret-garden-btn" onclick="toggleSecretGarden()">登 录</button>
</div>
</div>
<div class="content">
<!-- 添加/删除控制按钮 -->
<div class="add-remove-controls">
<button class="round-btn add-btn" onclick="showAddDialog()">+</button>
<button class="round-btn remove-btn" onclick="toggleRemoveMode()">-</button>
<button class="round-btn category-btn" onclick="addCategory()">C+</button>
<button class="round-btn remove-category-btn" onclick="toggleRemoveCategory()">C-</button>
</div>
<!-- 分类和卡片容器 -->
<div id="sections-container"></div>
<!-- 主题切换按钮 -->
<button id="theme-toggle" onclick="toggleTheme()">◑</button>
<!-- 添加链接对话框 -->
<div id="dialog-overlay">
<div id="dialog-box">
<label for="name-input">名称</label>
<input type="text" id="name-input">
<label for="url-input">地址</label>
<input type="text" id="url-input">
<label for="category-select">选择分类</label>
<select id="category-select"></select>
<div class="private-link-container">
<label for="private-checkbox">私密链接</label>
<input type="checkbox" id="private-checkbox">
</div>
<button onclick="addLink()">确定</button>
<button onclick="hideAddDialog()">取消</button>
</div>
</div>
<!-- 版权信息 -->
<div id="copyright" class="copyright">
<!--请不要删除-->
<p>项目地址:<a href="https://github.com/hmhm2022/Card-Tab" target="_blank">GitHub</a> 如果喜欢,烦请点个star!</p>
</div>
</div>
<script>
// 搜索引擎配置
const searchEngines = {
baidu: "https://www.baidu.com/s?wd=",
bing: "https://www.bing.com/search?q=",
google: "https://www.google.com/search?q="
};
let currentEngine = "baidu";
// 日志记录函数
function logAction(action, details) {
const timestamp = new Date().toISOString();
const logEntry = timestamp + ': ' + action + ' - ' + JSON.stringify(details);
console.log(logEntry);
}
// 设置当前搜索引擎
function setActiveEngine(engine) {
currentEngine = engine;
document.querySelectorAll('.search-engine').forEach(btn => {
btn.style.backgroundColor = btn.dataset.engine === engine ? '#c0c0c0' : '#f0f0f0';
});
logAction('设置搜索引擎', { engine });
}
// 搜索引擎按钮点击事件
document.querySelectorAll('.search-engine').forEach(button => {
button.addEventListener('click', () => setActiveEngine(button.dataset.engine));
});
// 搜索按钮点击事件
document.getElementById('search-button').addEventListener('click', () => {
const query = document.getElementById('search-input').value;
if (query) {
logAction('执行搜索', { engine: currentEngine, query });
window.open(searchEngines[currentEngine] + encodeURIComponent(query), '_blank');
}
});
// 搜索输入框回车事件
document.getElementById('search-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
document.getElementById('search-button').click();
}
});
// 初始化搜索引擎
setActiveEngine(currentEngine);
// 全局变量
let publicLinks = [];
let privateLinks = [];
let isAdmin = false;
let isLoggedIn = false;
let removeMode = false;
let isRemoveCategoryMode = false;
let isDarkTheme = false;
let links = [];
const categories = {};
// 添加新分类
async function addCategory() {
if (!await validateToken()) {
return;
}
const categoryName = prompt('请输入新分类名称:');
if (categoryName && !categories[categoryName]) {
categories[categoryName] = [];
updateCategorySelect();
renderCategories();
saveLinks();
logAction('添加分类', { categoryName, currentLinkCount: links.length });
} else if (categories[categoryName]) {
alert('该分类已存在');
logAction('添加分类失败', { categoryName, reason: '分类已存在' });
}
}
// 删除分类
async function deleteCategory(category) {
if (!await validateToken()) {
return;
}
if (confirm('确定要删除 "' + category + '" 分类吗?这将删除该分类下的所有链接。')) {
delete categories[category];
links = links.filter(link => link.category !== category);
publicLinks = publicLinks.filter(link => link.category !== category);
privateLinks = privateLinks.filter(link => link.category !== category);
updateCategorySelect();
saveLinks();
renderCategories();
logAction('删除分类', { category });
}
}
// 渲染分类(不重新加载链接)
function renderCategories() {
const container = document.getElementById('sections-container');
container.innerHTML = '';
Object.keys(categories).forEach(category => {
const section = document.createElement('div');
section.className = 'section';
const titleContainer = document.createElement('div');
titleContainer.className = 'section-title-container';
const title = document.createElement('div');
title.className = 'section-title';
title.textContent = category;
titleContainer.appendChild(title);
if (isAdmin) {
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '删除分类';
deleteBtn.className = 'delete-category-btn';
deleteBtn.style.display = isRemoveCategoryMode ? 'inline-block' : 'none';
deleteBtn.onclick = () => deleteCategory(category);
titleContainer.appendChild(deleteBtn);
}
const cardContainer = document.createElement('div');
cardContainer.className = 'card-container';
cardContainer.id = category;
section.appendChild(titleContainer);
section.appendChild(cardContainer);
container.appendChild(section);
const categoryLinks = links.filter(link => link.category === category);
categoryLinks.forEach(link => {
createCard(link, cardContainer);
});
});
logAction('渲染分类', { categoryCount: Object.keys(categories).length, linkCount: links.length });
}
// 读取链接数据
async function loadLinks() {
const headers = {
'Content-Type': 'application/json'
};
// 如果已登录,从 localStorage 获取 token 并添加到请求头
if (isLoggedIn) {
const token = localStorage.getItem('authToken');
if (token) {
headers['Authorization'] = token;
}
}
try {
const response = await fetch('/api/getLinks?userId=testUser', {
headers: headers
});
if (!response.ok) {
throw new Error("HTTP error! status: " + response.status);
}
const data = await response.json();
console.log('Received data:', data);
if (data.categories) {
Object.assign(categories, data.categories);
}
publicLinks = data.links ? data.links.filter(link => !link.isPrivate) : [];
privateLinks = data.links ? data.links.filter(link => link.isPrivate) : [];
links = isLoggedIn ? [...publicLinks, ...privateLinks] : publicLinks;
loadSections();
updateCategorySelect();
updateUIState();
logAction('读取链接', {
publicCount: publicLinks.length,
privateCount: privateLinks.length,
isLoggedIn: isLoggedIn,
hasToken: !!localStorage.getItem('authToken')
});
} catch (error) {
console.error('Error loading links:', error);
alert('加载链接时出错,请刷新页面重试');
}
}
// 更新UI状态
function updateUIState() {
const passwordInput = document.getElementById('admin-password');
const adminBtn = document.getElementById('admin-mode-btn');
const secretGardenBtn = document.getElementById('secret-garden-btn');
const addRemoveControls = document.querySelector('.add-remove-controls');
passwordInput.style.display = isLoggedIn ? 'none' : 'inline-block';
secretGardenBtn.textContent = isLoggedIn ? "退出" : "登录";
secretGardenBtn.style.display = 'inline-block';
if (isAdmin) {
adminBtn.textContent = "离开设置";
adminBtn.style.display = 'inline-block';
addRemoveControls.style.display = 'flex';
} else if (isLoggedIn) {
adminBtn.textContent = "设置";
adminBtn.style.display = 'inline-block';
addRemoveControls.style.display = 'none';
} else {
adminBtn.style.display = 'none';
addRemoveControls.style.display = 'none';
}
logAction('更新UI状态', { isAdmin, isLoggedIn });
}
// 登录状态显示(加载所有链接)
function showSecretGarden() {
if (isLoggedIn) {
links = [...publicLinks, ...privateLinks];
loadSections();
// 显示所有私密标签
document.querySelectorAll('.private-tag').forEach(tag => {
tag.style.display = 'block';
});
logAction('显示私密花园');
}
}
// 加载分类和链接
function loadSections() {
const container = document.getElementById('sections-container');
container.innerHTML = '';
Object.keys(categories).forEach(category => {
const section = document.createElement('div');
section.className = 'section';
const titleContainer = document.createElement('div');
titleContainer.className = 'section-title-container';
const title = document.createElement('div');
title.className = 'section-title';
title.textContent = category;
titleContainer.appendChild(title);
if (isAdmin) {
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '删除分类';
deleteBtn.className = 'delete-category-btn';
deleteBtn.style.display = 'none';
deleteBtn.onclick = () => deleteCategory(category);
titleContainer.appendChild(deleteBtn);
}
const cardContainer = document.createElement('div');
cardContainer.className = 'card-container';
cardContainer.id = category;
section.appendChild(titleContainer);
section.appendChild(cardContainer);
let privateCount = 0;
let linkCount = 0;
links.forEach(link => {
if (link.category === category) {
if (link.isPrivate) privateCount++;
linkCount++;
createCard(link, cardContainer);
}
});
if (privateCount < linkCount || isLoggedIn) {
container.appendChild(section);
}
});
logAction('加载分类和链接', { isAdmin: isAdmin, linkCount: links.length, categoryCount: Object.keys(categories).length });
}
// 创建卡片
function createCard(link, container) {
const card = document.createElement('div');
card.className = 'card';
card.setAttribute('draggable', isAdmin);
card.dataset.isPrivate = link.isPrivate;
const cardTop = document.createElement('div');
cardTop.className = 'card-top';
// 定义默认的 SVG 图标
const defaultIconSVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>' +
'<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>' +
'</svg>';
// 创建图标元素
const icon = document.createElement('img');
icon.className = 'card-icon';
// icon.src = 'https://api.iowen.cn/favicon/' + extractDomain(link.url) + '.png';
icon.src = 'https://www.faviconextractor.com/favicon/' + extractDomain(link.url);
icon.alt = 'Website Icon';
// 如果图片加载失败,使用默认的 SVG 图标
icon.onerror = function() {
const svgBlob = new Blob([defaultIconSVG], {type: 'image/svg+xml'});
const svgUrl = URL.createObjectURL(svgBlob);
this.src = svgUrl;
this.onload = () => URL.revokeObjectURL(svgUrl);
};
function extractDomain(url) {
let domain;
try {
domain = new URL(url).hostname;
} catch (e) {
domain = url;
}
return domain;
}
const title = document.createElement('div');
title.className = 'card-title';
title.textContent = link.name;
cardTop.appendChild(icon);
cardTop.appendChild(title);
const url = document.createElement('div');
url.className = 'card-url';
url.textContent = link.url;
card.appendChild(cardTop);
card.appendChild(url);
if (link.isPrivate) {
const privateTag = document.createElement('div');
privateTag.className = 'private-tag';
privateTag.textContent = '私密';
card.appendChild(privateTag);
}
const correctedUrl = link.url.startsWith('http://') || link.url.startsWith('https://') ? link.url : 'http://' + link.url;
if (!isAdmin) {
card.addEventListener('click', () => {
window.open(correctedUrl, '_blank');
logAction('打开链接', { name: link.name, url: correctedUrl });
});
}
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '–';
deleteBtn.className = 'delete-btn';
deleteBtn.onclick = function (event) {
event.stopPropagation();
removeCard(card);
};
card.appendChild(deleteBtn);
updateCardStyle(card);
card.addEventListener('dragstart', dragStart);
card.addEventListener('dragover', dragOver);
card.addEventListener('dragend', dragEnd);
card.addEventListener('drop', drop);
card.addEventListener('touchstart', touchStart, { passive: false });
if (isAdmin && removeMode) {
deleteBtn.style.display = 'block';
}
if (isAdmin || (link.isPrivate && isLoggedIn) || !link.isPrivate) {
container.appendChild(card);
}
// logAction('创建卡片', { name: link.name, isPrivate: link.isPrivate });
}
// 更新卡片样式
function updateCardStyle(card) {
if (isDarkTheme) {
card.style.backgroundColor = '#1e1e1e';
card.style.color = '#ffffff';
card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)';
} else {
card.style.backgroundColor = '#b8c9d9';
card.style.color = '#333';
card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)';
}
}
// 更新分类选择下拉框
function updateCategorySelect() {
const categorySelect = document.getElementById('category-select');
categorySelect.innerHTML = '';
Object.keys(categories).forEach(category => {
const option = document.createElement('option');
option.value = category;
option.textContent = category;
categorySelect.appendChild(option);
});
logAction('更新分类选择', { categoryCount: Object.keys(categories).length });
}
// 保存链接数据
async function saveLinks() {
if (isAdmin && !(await validateToken())) {
return;
}
let allLinks = [...publicLinks, ...privateLinks];
try {
await fetch('/api/saveOrder', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': localStorage.getItem('authToken')
},
body: JSON.stringify({
userId: 'testUser',
links: allLinks,
categories: categories
}),
});
logAction('保存链接', { linkCount: allLinks.length, categoryCount: Object.keys(categories).length });
} catch (error) {
logAction('保存链接失败', { error: error.message });
alert('保存链接失败,请重试');
}
}
// 添加卡片弹窗
async function addLink() {
if (!await validateToken()) {
return;
}
const name = document.getElementById('name-input').value;
const url = document.getElementById('url-input').value;
const category = document.getElementById('category-select').value;
const isPrivate = document.getElementById('private-checkbox').checked;
if (name && url && category) {
const newLink = { name, url, category, isPrivate };
if (isPrivate) {
privateLinks.push(newLink);
} else {
publicLinks.push(newLink);
}
links = isLoggedIn ? [...publicLinks, ...privateLinks] : publicLinks;
if (isAdmin || (isPrivate && isLoggedIn) || !isPrivate) {
const container = document.getElementById(category);
if (container) {
createCard(newLink, container);
} else {
categories[category] = [];
renderCategories();
}
}
saveLinks();
document.getElementById('name-input').value = '';
document.getElementById('url-input').value = '';
document.getElementById('private-checkbox').checked = false;
hideAddDialog();
logAction('添加卡片', { name, url, category, isPrivate });
}
}
// 删除卡片
async function removeCard(card) {
if (!await validateToken()) {
return;
}
const name = card.querySelector('.card-title').textContent;
const url = card.querySelector('.card-url').textContent;
const isPrivate = card.dataset.isPrivate === 'true';
links = links.filter(link => link.url !== url);
if (isPrivate) {
privateLinks = privateLinks.filter(link => link.url !== url);
} else {
publicLinks = publicLinks.filter(link => link.url !== url);
}
for (const key in categories) {
categories[key] = categories[key].filter(link => link.url !== url);
}
card.remove();
saveLinks();
logAction('删除卡片', { name, url, isPrivate });
}
// 拖拽卡片
let draggedCard = null;
let touchStartX, touchStartY;
// 触屏端拖拽卡片
function touchStart(event) {
if (!isAdmin) {
return;
}
draggedCard = event.target.closest('.card');
if (!draggedCard) return;
event.preventDefault();
const touch = event.touches[0];
touchStartX = touch.clientX;
touchStartY = touch.clientY;
draggedCard.classList.add('dragging');
document.addEventListener('touchmove', touchMove, { passive: false });
document.addEventListener('touchend', touchEnd);
}
function touchMove(event) {
if (!draggedCard) return;
event.preventDefault();
const touch = event.touches[0];
const currentX = touch.clientX;
const currentY = touch.clientY;
const deltaX = currentX - touchStartX;
const deltaY = currentY - touchStartY;
draggedCard.style.transform = "translate(" + deltaX + "px, " + deltaY + "px)";
const target = findCardUnderTouch(currentX, currentY);
if (target && target !== draggedCard) {
const container = target.parentElement;
const targetRect = target.getBoundingClientRect();
if (currentX < targetRect.left + targetRect.width / 2) {
container.insertBefore(draggedCard, target);
} else {
container.insertBefore(draggedCard, target.nextSibling);
}
}
}
function touchEnd(event) {
if (!draggedCard) return;
const card = draggedCard;
const targetCategory = card.closest('.card-container').id;
validateToken().then(isValid => {
if (isValid && card) {
updateCardCategory(card, targetCategory);
saveCardOrder().catch(error => {
console.error('Save failed:', error);
});
}
cleanupDragState();
});
}
function findCardUnderTouch(x, y) {
const cards = document.querySelectorAll('.card:not(.dragging)');
return Array.from(cards).find(card => {
const rect = card.getBoundingClientRect();
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
});
}
// PC端拖拽卡片
function dragStart(event) {
if (!isAdmin) {
event.preventDefault();
return;
}
draggedCard = event.target.closest('.card');
if (!draggedCard) return;
draggedCard.classList.add('dragging');
event.dataTransfer.effectAllowed = "move";
logAction('开始拖拽卡片', { name: draggedCard.querySelector('.card-title').textContent });
}
function dragOver(event) {
if (!isAdmin) {
event.preventDefault();
return;
}
event.preventDefault();
const target = event.target.closest('.card');
if (target && target !== draggedCard) {
const container = target.parentElement;
const mousePositionX = event.clientX;
const targetRect = target.getBoundingClientRect();
if (mousePositionX < targetRect.left + targetRect.width / 2) {
container.insertBefore(draggedCard, target);
} else {
container.insertBefore(draggedCard, target.nextSibling);
}
}
}
// 清理拖拽状态函数
function cleanupDragState() {
if (draggedCard) {
draggedCard.classList.remove('dragging');
draggedCard.style.transform = '';
draggedCard = null;
}
document.removeEventListener('touchmove', touchMove);
document.removeEventListener('touchend', touchEnd);
touchStartX = null;
touchStartY = null;
}
// PC端拖拽结束
function drop(event) {
if (!isAdmin) {
event.preventDefault();
return;
}
event.preventDefault();
const card = draggedCard;
const targetCategory = event.target.closest('.card-container').id;
validateToken().then(isValid => {
if (isValid && card) {
updateCardCategory(card, targetCategory);
saveCardOrder().catch(error => {
console.error('Save failed:', error);
});
}
cleanupDragState();
});
}
function dragEnd(event) {
if (draggedCard) {
draggedCard.classList.remove('dragging');
logAction('拖拽卡片结束');
}
}
// 更新卡片分类
function updateCardCategory(card, newCategory) {
const cardTitle = card.querySelector('.card-title').textContent;
const cardUrl = card.querySelector('.card-url').textContent;
const isPrivate = card.dataset.isPrivate === 'true';
const linkIndex = links.findIndex(link => link.url === cardUrl);
if (linkIndex !== -1) {
links[linkIndex].category = newCategory;
}
const linkArray = isPrivate ? privateLinks : publicLinks;
const arrayIndex = linkArray.findIndex(link => link.url === cardUrl);
if (arrayIndex !== -1) {
linkArray[arrayIndex].category = newCategory;
}
card.dataset.category = newCategory;
}
// 在页面加载完成后添加触摸事件监听器
document.addEventListener('DOMContentLoaded', function() {
const cardContainers = document.querySelectorAll('.card-container');
cardContainers.forEach(container => {
container.addEventListener('touchstart', touchStart, { passive: false });
});
});
// 保存卡片顺序
async function saveCardOrder() {
if (!await validateToken()) {
return;
}
const containers = document.querySelectorAll('.card-container');
let newPublicLinks = [];
let newPrivateLinks = [];
let newCategories = {};
containers.forEach(container => {
const category = container.id;
newCategories[category] = [];
[...container.children].forEach(card => {
const url = card.querySelector('.card-url').textContent;
const name = card.querySelector('.card-title').textContent;
const isPrivate = card.dataset.isPrivate === 'true';
card.dataset.category = category;
const link = { name, url, category, isPrivate };
if (isPrivate) {
newPrivateLinks.push(link);
} else {
newPublicLinks.push(link);
}
newCategories[category].push(link);
});
});
publicLinks.length = 0;
publicLinks.push(...newPublicLinks);
privateLinks.length = 0;
privateLinks.push(...newPrivateLinks);
Object.keys(categories).forEach(key => delete categories[key]);
Object.assign(categories, newCategories);
logAction('保存卡片顺序', {
publicCount: newPublicLinks.length,
privateCount: newPrivateLinks.length,
categoryCount: Object.keys(newCategories).length
});
try {
const response = await fetch('/api/saveOrder', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': localStorage.getItem('authToken')
},
body: JSON.stringify({
userId: 'testUser',
links: [...newPublicLinks, ...newPrivateLinks],
categories: newCategories
}),
});
const result = await response.json();
if (!result.success) {
throw new Error('Failed to save order');
}
logAction('保存卡片顺序', { publicCount: newPublicLinks.length, privateCount: newPrivateLinks.length, categoryCount: Object.keys(newCategories).length });
} catch (error) {
logAction('保存顺序失败', { error: error.message });
alert('保存顺序失败,请重试');
}
}
// 设置状态重新加载卡片
function reloadCardsAsAdmin() {
document.querySelectorAll('.card-container').forEach(container => {
container.innerHTML = '';
});
loadLinks().then(() => {
if (isDarkTheme) {
applyDarkTheme();
}
});
logAction('重新加载卡片(管理员模式)');
}
// 密码输入框回车事件
document.getElementById('admin-password').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
toggleSecretGarden();
}
});
// 切换设置状态
async function toggleAdminMode() {
const adminBtn = document.getElementById('admin-mode-btn');
const addRemoveControls = document.querySelector('.add-remove-controls');
if (!isAdmin && isLoggedIn) {
if (!await validateToken()) {
return;
}
// 在进入设置模式之前进行备份
try {
const response = await fetch('/api/backupData', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': localStorage.getItem('authToken')
},
body: JSON.stringify({
sourceUserId: 'testUser',
backupUserId: 'backup'
}),
});
const result = await response.json();
if (result.success) {
logAction('数据备份成功');
} else {
throw new Error('备份失败');
}
} catch (error) {
logAction('数据备份失败', { error: error.message });
if (!confirm('备份失败,是否仍要继续进入设置模式?')) {
return;
}
}
isAdmin = true;
adminBtn.textContent = "退出设置";
addRemoveControls.style.display = 'flex';
alert('准备设置分类和书签');
reloadCardsAsAdmin();
logAction('进入设置');
} else if (isAdmin) {
isAdmin = false;
removeMode = false;
adminBtn.textContent = "设 置";
addRemoveControls.style.display = 'none';
alert('设置已保存');
reloadCardsAsAdmin();
logAction('离开设置');
}
updateUIState();
}
// 切换到登录状态
function toggleSecretGarden() {
const passwordInput = document.getElementById('admin-password');
if (!isLoggedIn) {
verifyPassword(passwordInput.value)
.then(result => {
if (result.valid) {
isLoggedIn = true;
localStorage.setItem('authToken', result.token);
console.log('Token saved:', result.token);
loadLinks();
alert('登录成功!');
logAction('登录成功');
} else {
alert('密码错误');
logAction('登录失败', { reason: result.error || '密码错误' });
}
updateUIState();
})
.catch(error => {
console.error('Login error:', error);
alert('登录过程出错,请重试');
});
} else {
isLoggedIn = false;
isAdmin = false;
localStorage.removeItem('authToken');
links = publicLinks;
loadSections();
alert('退出登录!');
updateUIState();
passwordInput.value = '';
logAction('退出登录');
}
}
// 应用暗色主题
function applyDarkTheme() {
document.body.style.backgroundColor = '#121212';
document.body.style.color = '#ffffff';
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
card.style.backgroundColor = '#1e1e1e';
card.style.color = '#ffffff';
card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)';
});
logAction('应用暗色主题');
}
// 显示添加链接对话框
function showAddDialog() {
document.getElementById('dialog-overlay').style.display = 'flex';
logAction('显示添加链接对话框');
}
// 隐藏添加链接对话框
function hideAddDialog() {
document.getElementById('dialog-overlay').style.display = 'none';
logAction('隐藏添加链接对话框');
}
// 切换删除卡片模式
function toggleRemoveMode() {
removeMode = !removeMode;
const deleteButtons = document.querySelectorAll('.delete-btn');
deleteButtons.forEach(btn => {
btn.style.display = removeMode ? 'block' : 'none';
});
logAction('切换删除卡片模式', { removeMode });
}
//切换删除分类模式
function toggleRemoveCategory() {
isRemoveCategoryMode = !isRemoveCategoryMode;
const deleteButtons = document.querySelectorAll('.delete-category-btn');
deleteButtons.forEach(btn => {
btn.style.display = isRemoveCategoryMode ? 'inline-block' : 'none';
});
logAction('切换删除分类模式', { isRemoveCategoryMode });
}
// 切换主题
function toggleTheme() {
isDarkTheme = !isDarkTheme;
document.body.style.backgroundColor = isDarkTheme ? '#121212' : '#e8f4ea';
document.body.style.color = isDarkTheme ? '#ffffff' : '#333';
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
card.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#b8c9d9';
card.style.color = isDarkTheme ? '#ffffff' : '#333';
card.style.boxShadow = isDarkTheme
? '0 4px 8px rgba(0, 0, 0, 0.5)'
: '0 4px 8px rgba(0, 0, 0, 0.1)';
});
const fixedElements = document.querySelectorAll('.fixed-elements');
fixedElements.forEach(element => {
element.style.backgroundColor = isDarkTheme ? '#121212' : '#e8f4ea';
element.style.color = isDarkTheme ? '#ffffff' : '#333';
});
const dialogBox = document.getElementById('dialog-box');
dialogBox.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#ffffff';
dialogBox.style.color = isDarkTheme ? '#ffffff' : '#333';
const inputs = document.querySelectorAll('input[type="text"], input[type="password"], select');
inputs.forEach(input => {
input.style.backgroundColor = isDarkTheme ? '#444' : '#fff';
input.style.color = isDarkTheme ? '#fff' : '#333';
input.style.borderColor = isDarkTheme ? '#555' : '#ccc';
});
logAction('切换主题', { isDarkTheme });
}
// 验证密码
async function verifyPassword(inputPassword) {
const response = await fetch('/api/verifyPassword', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: inputPassword }),
});
const result = await response.json();
return result;
}
// 初始化加载
document.addEventListener('DOMContentLoaded', async () => {
await validateToken();
loadLinks();
});
// 前端检查是否有 token
async function validateToken() {
const token = localStorage.getItem('authToken');
if (!token) {
isLoggedIn = false;
updateUIState();
return false;
}
try {
const response = await fetch('/api/getLinks?userId=testUser', {
headers: { 'Authorization': token }
});
if (response.status === 401) {
await resetToLoginState('token已过期,请重新登录');
return false;
}
isLoggedIn = true;
updateUIState();
return true;
} catch (error) {
console.error('Token validation error:', error);
return false;
}
}
// 重置状态
async function resetToLoginState(message) {
alert(message);
cleanupDragState();
localStorage.removeItem('authToken');
isLoggedIn = false;
isAdmin = false;
removeMode = false;
isRemoveCategoryMode = false;
const passwordInput = document.getElementById('admin-password');
if (passwordInput) {
passwordInput.value = '';
}
updateUIState();
links = publicLinks;
loadSections();
const addRemoveControls = document.querySelector('.add-remove-controls');
if (addRemoveControls) {
addRemoveControls.style.display = 'none';
}
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.style.display = 'none';
});
document.querySelectorAll('.delete-category-btn').forEach(btn => {
btn.style.display = 'none';
});
const dialogOverlay = document.getElementById('dialog-overlay');
if (dialogOverlay) {
dialogOverlay.style.display = 'none';
}
}
</script>
</body>
</html>
`;
// 服务端 token 验证
async function validateServerToken(authToken, env) {
if (!authToken) {
return {
isValid: false,
status: 401,
response: { error: 'Unauthorized', message: '未登录或登录已过期' }
};
}
try {
const [timestamp, hash] = authToken.split('.');
const tokenTimestamp = parseInt(timestamp);
const now = Date.now();
const FIFTEEN_MINUTES = 15 * 60 * 1000;
if (now - tokenTimestamp > FIFTEEN_MINUTES) {
return {
isValid: false,
status: 401,
response: {
error: 'Token expired',
tokenExpired: true,
message: '登录已过期,请重新登录'
}
};
}
const tokenData = timestamp + "_" + env.ADMIN_PASSWORD;
const encoder = new TextEncoder();
const data = encoder.encode(tokenData);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const expectedHash = btoa(String.fromCharCode(...new Uint8Array(hashBuffer)));
if (hash !== expectedHash) {
return {
isValid: false,
status: 401,
response: {
error: 'Invalid token',
tokenInvalid: true,
message: '登录状态无效,请重新登录'
}
};
}
return { isValid: true };
} catch (error) {
return {
isValid: false,
status: 401,
response: {
error: 'Invalid token',
tokenInvalid: true,
message: '登录验证失败,请重新登录'
}
};
}
}
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === '/') {
return new Response(HTML_CONTENT, {
headers: { 'Content-Type': 'text/html' }
});
}
if (url.pathname === '/api/getLinks') {
const userId = url.searchParams.get('userId');
const authToken = request.headers.get('Authorization');
const data = await env.CARD_ORDER.get(userId);
if (data) {
const parsedData = JSON.parse(data);
// 验证 token
if (authToken) {
const validation = await validateServerToken(authToken, env);
if (!validation.isValid) {
return new Response(JSON.stringify(validation.response), {
status: validation.status,
headers: { 'Content-Type': 'application/json' }
});
}
// Token 有效,返回完整数据
return new Response(JSON.stringify(parsedData), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
// 未提供 token,只返回公开数据
const filteredLinks = parsedData.links.filter(link => !link.isPrivate);
const filteredCategories = {};
Object.keys(parsedData.categories).forEach(category => {
filteredCategories[category] = parsedData.categories[category].filter(link => !link.isPrivate);
});
return new Response(JSON.stringify({
links: filteredLinks,
categories: filteredCategories
}), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
return new Response(JSON.stringify({
links: [],
categories: {}
}), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
if (url.pathname === '/api/saveOrder' && request.method === 'POST') {
const authToken = request.headers.get('Authorization');
const validation = await validateServerToken(authToken, env);
if (!validation.isValid) {
return new Response(JSON.stringify(validation.response), {
status: validation.status,
headers: { 'Content-Type': 'application/json' }
});
}
const { userId, links, categories } = await request.json();
await env.CARD_ORDER.put(userId, JSON.stringify({ links, categories }));
return new Response(JSON.stringify({
success: true,
message: '保存成功'
}), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
if (url.pathname === '/api/verifyPassword' && request.method === 'POST') {
try {
const { password } = await request.json();
const isValid = password === env.ADMIN_PASSWORD;
if (isValid) {
// 生成包含时间戳的加密 token
const timestamp = Date.now();
const tokenData = timestamp + "_" + env.ADMIN_PASSWORD;
const encoder = new TextEncoder();
const data = encoder.encode(tokenData);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
// 使用指定格式:timestamp.hash
const token = timestamp + "." + btoa(String.fromCharCode(...new Uint8Array(hashBuffer)));
return new Response(JSON.stringify({
valid: true,
token: token
}), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
return new Response(JSON.stringify({
valid: false,
error: 'Invalid password'
}), {
status: 403,
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
return new Response(JSON.stringify({
valid: false,
error: error.message
}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
}
if (url.pathname === '/api/backupData' && request.method === 'POST') {
const { sourceUserId } = await request.json();
const result = await this.backupData(env, sourceUserId);
return new Response(JSON.stringify(result), {
status: result.success ? 200 : 404,
headers: { 'Content-Type': 'application/json' }
});
}
return new Response('Not Found', { status: 404 });
},
async backupData(env, sourceUserId) {
const MAX_BACKUPS = 10;
const sourceData = await env.CARD_ORDER.get(sourceUserId);
if (sourceData) {
try {
const currentDate = new Date().toLocaleString('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}).replace(/\//g, '-');
const backupId = `backup_${currentDate}`;
const backups = await env.CARD_ORDER.list({ prefix: 'backup_' });
const backupKeys = backups.keys.map(key => key.name).sort((a, b) => {
const timeA = new Date(a.split('_')[1].replace(/-/g, '/')).getTime();
const timeB = new Date(b.split('_')[1].replace(/-/g, '/')).getTime();
return timeB - timeA; // 降序排序,最新的在前
});
await env.CARD_ORDER.put(backupId, sourceData);
const allBackups = [...backupKeys, backupId].sort((a, b) => {
const timeA = new Date(a.split('_')[1].replace(/-/g, '/')).getTime();
const timeB = new Date(b.split('_')[1].replace(/-/g, '/')).getTime();
return timeB - timeA;
});
const backupsToDelete = allBackups.slice(MAX_BACKUPS);
if (backupsToDelete.length > 0) {
await Promise.all(
backupsToDelete.map(key => env.CARD_ORDER.delete(key))
);
}
return {
success: true,
backupId,
remainingBackups: MAX_BACKUPS,
deletedCount: backupsToDelete.length
};
} catch (error) {
return {
success: false,
error: 'Backup operation failed',
details: error.message
};
}
}
return { success: false, error: 'Source data not found' };
}
};
修改后workes, 效果
const HTML_CONTENT = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ws01主页</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2280%22>💠</text></svg>">
<style>
/* 全局样式 */
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #d4d4d4;
transition: background-color 0.3s ease;
}
ul {
padding: 0;
margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 0px;
margin-inline-end: 0px;
padding-inline-start: 40px;
unicode-bidi: isolate;
}
li {
display: list-item;
text-align: -webkit-match-parent;
unicode-bidi: isolate;
margin: 0 8px;
}
.background {
background-image: linear-gradient(#d4d4d4 1px, transparent 0), linear-gradient(90deg, #d4d4d4 1px, transparent 0);
background-size: 32px 32px;
background-color: #fffcf8;
}
.header {
background-color: #fff;
box-shadow: 0 0 5px rgba(0, 0, 0, .1);
transition: background-color .5s;
}
.navbar {
display: flex;
width: 1280px;
margin: auto;
height: 40px;
}
.navbar .brand {
display: flex;
align-items: center;
color: #555;
}
.brand .logo {
max-width: 36px;
}
.brand .title {
margin-left: 5px;
font-family: helvetica neue, helvetica, arial, sans-serif;
font-size: 24px;
font-weight: 700;
}
.beta {
color: #ccc;
font-size: 11px;
font-weight: 400;
position: relative;
top: -14px;
}
.category-list {
list-style: none;
display: flex;
align-items: center;
}
.sites {
width:1286px;
background:;
border:2px solid auto;
margin:15px auto;
padding:0px;
text-align:center;
}
.sites1 {
width:1286px;
background:;
border:2px solid auto;
margin:15px auto;
padding:0px;
}
.sites dl {
height:36px;
line-height:36px;
display:block;
margin:0;
}
.sites dl.alt {
background:#d4d4d4;
border-top:1px solid #ffffff;
border-bottom:1px solid #ffffff;
}
.sites dl.alt2 {
background:#d4d4d4;
border-top:1px solid #ffffff;
border-bottom:1px solid #ffffff;
}
.sites dt,.sites dd {
text-align:center;
display:block;
float:left;
}
.sites dt {
width:60px;
}
.sites dd {
width:90px;
margin:0;
}
.footer {
width:580px;
text-align:center;
margin:5px auto;
padding:2px;
}
/* 固定元素样式 */
.fixed-elements {
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: #d4d4d4;
z-index: 1000;
padding: 10px;
transition: background-color 0.3s ease;
height: 130px;
}
.fixed-elements h3 {
position: absolute;
top: 10px;
left: 20px;
margin: 0;
}
/* 中心内容样式 */
.center-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
max-width: 600px;
text-align: center;
}
/* 管理员控制面板样式 */
.admin-controls {
position: fixed;
top: 10px;
right: 10px;
font-size: 60%;
}
/* 添加/删除控制按钮样式 */
.add-remove-controls {
display: none;
flex-direction: column;
position: fixed;
right: 20px;
top: 50%;
transform: translateY(-50%);
align-items: center;
gap: 10px;
}
.round-btn {
background-color: #007bff;
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
text-align: center;
font-size: 24px;
line-height: 40px;
cursor: pointer;
margin: 5px 0;
}
.add-btn { order: 1; }
.remove-btn { order: 2; }
.category-btn { order: 3; }
.remove-category-btn { order: 4; }
/* 主要内容区域样式 */
.content {
margin-top: 140px;
padding: 20px;
}
/* 搜索栏样式 */
.search-container {
margin-top: 10px;
}
.search-bar {
display: flex;
justify-content: center;
margin-bottom: 10px;
}
.search-bar input {
width: 50%;
padding: 5px;
border: 1px solid #ccc;
border-radius: 5px 0 0 5px;
}
.search-bar button {
padding: 5px 20px;
border: 1px solid #ccc;
border-left: none;
background-color: rgba(160, 201, 229, 0.6);
border-radius: 0 5px 5px 0;
cursor: pointer;
}
/* 搜索引擎按钮样式 */
.search-engines {
display: flex;
justify-content: center;
gap: 10px;
}
.search-engine {
padding: 5px 10px;
border: 1px solid #ccc;
background-color: #f0f0;
border-radius: 5px;
cursor: pointer;
}
/* 主题切换按钮样式 */
#theme-toggle {
position: fixed;
bottom: 50px;
right: 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
text-align: center;
font-size: 24px;
line-height: 40px;
cursor: pointer;
}
/* 显示日志按钮样式 */
#view-logs-btn {
position: fixed;
top: 100px;
right: 10px;
z-index: 1000;
}
/* 对话框样式 */
#dialog-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
}
#dialog-box {
background-color: white;
padding: 20px;
border-radius: 5px;
width: 300px;
}
#dialog-box input, #dialog-box select {
width: 100%;
margin-bottom: 10px;
padding: 5px;
}
/* 分类和卡片样式 */
.section {
margin-bottom: 20px;
}
.section-title-container {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.section-title {
font-size: 18px;
font-weight: bold;
margin-left: 20px; /* 分类标签右移添加这一行 */
color: green; /* 分类标签字体颜色添加这一行 */
}
.delete-category-btn {
background-color: pink;
color: white;
border: none;
padding: 5px 10px;
border-radius: 5px;
cursor: pointer;
}
.card-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.card {
background-color: rgba(160, 201, 229, 0.6);
border-radius: 5px;
padding: 10px;
width: 132px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s;
position: relative;
}
/* 中等屏幕 (平板等) */
@media (max-width: 1024px) {
}
/* 小屏幕 (手机) */
@media (max-width: 768px) {
}
.card:hover {
transform: translateY(-5px);
}
.card-top {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.card-icon {
width: 20px;
height: 20px;
margin-right: 5px;
}
.card-title {
font-size: 18px;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-url {
font-size: 14px;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: left; /* 添加左对齐 */
}
/* 超小屏幕 (更小手机) */
@media (max-width: 480px) {
.card {
padding: 6px; /* 进一步减小卡片内边距6 */
width: 300px; /* 手机上显示4列,235时显示6列 */
}
.card-icon {
width: 42px;
height: 42px;
}
.card-title {
font-size: 36px;
}
.card-url {
font-size: 22px;
}
}
.private-tag {
background-color: #ff9800;
color: white;
font-size: 10px;
padding: 2px 5px;
border-radius: 3px;
position: absolute;
top: 5px;
right: 5px;
}
.delete-btn {
position: absolute;
top: -10px;
right: -10px;
background-color: red;
color: white;
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
text-align: center;
font-size: 14px;
line-height: 20px;
cursor: pointer;
display: none;
}
/* 版权信息样式 */
#copyright {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 40px;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
z-index: 1000;
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
}
#copyright p {
margin: 0;
}
#copyright a {
color: #007bff;
text-decoration: none;
}
#copyright a:hover {
text-decoration: underline;
}
</style>
</head>
<body class="background">
</div>
<div class="sites"> <!-- 00 -->
<header class="header">
<nav class="navbar">
<a href="https://www.199881.xyz/" class="brand">
<img class="debug logo" src="https://cdn.glitch.global/efdace30-a873-49c7-aaa9-4fa31679ee0c/thumbnails%2F%E5%9B%BE%E6%A0%8701.jpg?1692046715299">
<span class="debug title">WS01の主页</span>
<span class="debug beta">beta</span>
<!-- 一言模块 -->
<p id="hitokoto">
<a href="#" id="hitokoto_text"></a>
</p>
<script src="https://v1.hitokoto.cn/?encode=js&select=%23hitokoto" defer></script>
</a>
</header>
<!-- 搜索栏 -->
<div class="search-container">
<div class="search-bar">
<input type="text" id="search-input" placeholder="">
<button id="search-button">🔍</button>
</div>
<div class="search-engines">
<button class="search-engine" data-engine="baidu">百度</button>
<button class="search-engine" data-engine="bing">必应</button>
<button class="search-engine" data-engine="toutiao">头条</button>
<button class="search-engine" data-engine="sm">神马</button>
<button class="search-engine" data-engine="bilibili">哔哩</button>
<button class="search-engine" data-engine="github">github</button>
<button class="search-engine" data-engine="google">谷歌</button>
<button class="search-engine" data-engine="yandex">yandex</button>
</div>
</div>
<div class="head">
<!-- 添加/删除控制按钮 -->
<div class="add-remove-controls">
<button class="round-btn add-btn" onclick="showAddDialog()">+</button>
<button class="round-btn remove-btn" onclick="toggleRemoveMode()">-</button>
<button class="round-btn category-btn" onclick="addCategory()">C+</button>
<button class="round-btn remove-category-btn" onclick="toggleRemoveCategory()">C-</button>
</div>
<div class="sites1"> <!-- 00 -->
<!-- 分类和卡片容器 -->
<div id="sections-container"></div>
<!-- 主题切换按钮 -->
<button id="theme-toggle" onclick="toggleTheme()">◑</button>
<!-- 显示日志按钮 用于调试-->
<!--<button id="view-logs-btn" onclick="viewLogs()">显示日志</button>-->
<!-- 添加链接对话框 -->
<div id="dialog-overlay">
<div id="dialog-box">
<label for="name-input">名称</label>
<input type="text" id="name-input">
<label for="url-input">地址</label>
<input type="text" id="url-input">
<label for="category-select">选择分类</label>
<select id="category-select"></select>
<div class="private-link-container">
<label for="private-checkbox">私密链接</label>
<input type="checkbox" id="private-checkbox">
</div>
<button onclick="addLink()">确定</button>
<button onclick="hideAddDialog()">取消</button>
</div>
</div>
<br />
<!-- body 页脚 -->
<div class="footer">
<!-- 管理员控制面板 -->
<input type="password" id="admin-password" placeholder="输入密码">
<button id="admin-mode-btn" onclick="toggleAdminMode()">设 置</button>
<button id="secret-garden-btn" onclick="toggleSecretGarden()">登 录</button>
<br />
<br /> <br />
<!-- 版权信息 -->
<div id="copyright" class="copyright">
<!--请不要删除-->
<p><a href="https://github.com/hmhm2022/Card-Tab" target="_blank">GitHub</a> 项目
<!-- 开站时间开始 -->
<span id="timeDate">载入天数...</span><span id="times">载入时分秒...</span> <script language="javascript">
var now = new Date();
function createtime(){
var grt= new Date("09/05/2024 00:00:00");/*---这里是网站的启用时间--*/
now.setTime(now.getTime()+250);
days = (now - grt ) / 1000 / 60 / 60 / 24;
dnum = Math.floor(days);
hours = (now - grt ) / 1000 / 60 / 60 - (24 * dnum);
hnum = Math.floor(hours);
if(String(hnum).length ==1 ){hnum = "0" + hnum;}
minutes = (now - grt ) / 1000 /60 - (24 * 60 * dnum) - (60 * hnum);
mnum = Math.floor(minutes);
if(String(mnum).length ==1 ){mnum = "0" + mnum;}
seconds = (now - grt ) / 1000 - (24 * 60 * 60 * dnum) - (60 * 60 * hnum) - (60 * mnum);
snum = Math.round(seconds);
if(String(snum).length ==1 ){snum = "0" + snum;}
document.getElementById("timeDate").innerHTML = "稳定运行"+dnum+"天";
document.getElementById("times").innerHTML = hnum + "小时" + mnum + "分" + snum + "秒";
}
setInterval("createtime()",250);
</script>
<!-- 开站时间结束 -->
<!-- <script defer src="https://four-root-occupation.glitch.me/denglong.js"></script> 国庆快乐-->
</p>
</div>
</div>
<script>
// 搜索引擎配置
const searchEngines = {
baidu: "https://www.baidu.com/s?wd=",
bing: "https://www.bing.com/search?q=",
sm: "https://m.sm.cn/s?q=",
toutiao: "https://so.toutiao.com/search?dvpf=pc&source=trending_card&keyword=",
bilibili: "https://search.bilibili.com/all?keyword=",
github: "https://github.com/search?q=",
google: "https://www.google.com/search?q=",
yandex: "https://www.yandex.com/search/?text="
};
let currentEngine = "baidu";
// 日志记录函数
function logAction(action, details) {
const timestamp = new Date().toISOString();
const logEntry = timestamp + ': ' + action + ' - ' + JSON.stringify(details);
let logs = JSON.parse(localStorage.getItem('cardTabLogs') || '[]');
logs.push(logEntry);
// 保留最新的1000条日志
if (logs.length > 1000) {
logs = logs.slice(-1000);
}
localStorage.setItem('cardTabLogs', JSON.stringify(logs));
console.log(logEntry); // 同时在控制台输出日志
}
// 查看日志的函数
function viewLogs() {
const logs = JSON.parse(localStorage.getItem('cardTabLogs') || '[]');
console.log('Card Tab Logs:');
logs.forEach(log => console.log(log));
alert('日志已在控制台输出,请按F12打开开发者工具查看。');
}
// 设置当前搜索引擎
function setActiveEngine(engine) {
currentEngine = engine;
document.querySelectorAll('.search-engine').forEach(btn => {
btn.style.backgroundColor = btn.dataset.engine === engine ? '#c0c0c0' : '#f0f0f0';
});
logAction('设置搜索引擎', { engine });
}
// 搜索引擎按钮点击事件
document.querySelectorAll('.search-engine').forEach(button => {
button.addEventListener('click', () => setActiveEngine(button.dataset.engine));
});
// 搜索按钮点击事件
document.getElementById('search-button').addEventListener('click', () => {
const query = document.getElementById('search-input').value;
if (query) {
logAction('执行搜索', { engine: currentEngine, query });
window.open(searchEngines[currentEngine] + encodeURIComponent(query), '_blank');
}
});
// 搜索输入框回车事件
document.getElementById('search-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
document.getElementById('search-button').click();
}
});
// 初始化搜索引擎
setActiveEngine(currentEngine);
// 全局变量
let publicLinks = [];
let privateLinks = [];
let isAdmin = false;
let isLoggedIn = false;
let removeMode = false;
let isRemoveCategoryMode = false;
let isDarkTheme = false;
let links = [];
const categories = {};
// 添加新分类
async function addCategory() {
if (!await validateToken()) {
return;
}
const categoryName = prompt('请输入新分类名称:');
if (categoryName && !categories[categoryName]) {
categories[categoryName] = [];
updateCategorySelect();
renderCategories();
saveLinks();
logAction('添加分类', { categoryName, currentLinkCount: links.length });
} else if (categories[categoryName]) {
alert('该分类已存在');
logAction('添加分类失败', { categoryName, reason: '分类已存在' });
}
}
// 删除分类
async function deleteCategory(category) {
if (!await validateToken()) {
return;
}
if (confirm('确定要删除 "' + category + '" 分类吗?这将删除该分类下的所有链接。')) {
delete categories[category];
links = links.filter(link => link.category !== category);
publicLinks = publicLinks.filter(link => link.category !== category);
privateLinks = privateLinks.filter(link => link.category !== category);
updateCategorySelect();
saveLinks();
renderCategories();
logAction('删除分类', { category });
}
}
// 渲染分类(不重新加载链接)
function renderCategories() {
const container = document.getElementById('sections-container');
container.innerHTML = '';
Object.keys(categories).forEach(category => {
const section = document.createElement('div');
section.className = 'section';
const titleContainer = document.createElement('div');
titleContainer.className = 'section-title-container';
const title = document.createElement('div');
title.className = 'section-title';
title.textContent = category;
titleContainer.appendChild(title);
if (isAdmin) {
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '删除分类';
deleteBtn.className = 'delete-category-btn';
deleteBtn.style.display = isRemoveCategoryMode ? 'inline-block' : 'none';
deleteBtn.onclick = () => deleteCategory(category);
titleContainer.appendChild(deleteBtn);
}
const cardContainer = document.createElement('div');
cardContainer.className = 'card-container';
cardContainer.id = category;
section.appendChild(titleContainer);
section.appendChild(cardContainer);
container.appendChild(section);
const categoryLinks = links.filter(link => link.category === category);
categoryLinks.forEach(link => {
createCard(link, cardContainer);
});
});
logAction('渲染分类', { categoryCount: Object.keys(categories).length, linkCount: links.length });
}
// 读取链接数据
async function loadLinks() {
const headers = {
'Content-Type': 'application/json'
};
// 如果已登录,从 localStorage 获取 token 并添加到请求头
if (isLoggedIn) {
const token = localStorage.getItem('authToken');
if (token) {
headers['Authorization'] = token;
}
}
try {
const response = await fetch('/api/getLinks?userId=testUser', {
headers: headers
});
if (!response.ok) {
throw new Error("HTTP error! status: " + response.status);
}
const data = await response.json();
console.log('Received data:', data);
if (data.categories) {
Object.assign(categories, data.categories);
}
publicLinks = data.links ? data.links.filter(link => !link.isPrivate) : [];
privateLinks = data.links ? data.links.filter(link => link.isPrivate) : [];
links = isLoggedIn ? [...publicLinks, ...privateLinks] : publicLinks;
loadSections();
updateCategorySelect();
updateUIState();
logAction('读取链接', {
publicCount: publicLinks.length,
privateCount: privateLinks.length,
isLoggedIn: isLoggedIn,
hasToken: !!localStorage.getItem('authToken')
});
} catch (error) {
console.error('Error loading links:', error);
alert('加载链接时出错,请刷新页面重试');
}
}
// 更新UI状态
function updateUIState() {
const passwordInput = document.getElementById('admin-password');
const adminBtn = document.getElementById('admin-mode-btn');
const secretGardenBtn = document.getElementById('secret-garden-btn');
const addRemoveControls = document.querySelector('.add-remove-controls');
passwordInput.style.display = isLoggedIn ? 'none' : 'inline-block';
secretGardenBtn.textContent = isLoggedIn ? "退出" : "登录";
secretGardenBtn.style.display = 'inline-block';
if (isAdmin) {
adminBtn.textContent = "离开设置";
adminBtn.style.display = 'inline-block';
addRemoveControls.style.display = 'flex';
} else if (isLoggedIn) {
adminBtn.textContent = "设置";
adminBtn.style.display = 'inline-block';
addRemoveControls.style.display = 'none';
} else {
adminBtn.style.display = 'none';
addRemoveControls.style.display = 'none';
}
logAction('更新UI状态', { isAdmin, isLoggedIn });
}
// 登录状态显示(加载所有链接)
function showSecretGarden() {
if (isLoggedIn) {
links = [...publicLinks, ...privateLinks];
loadSections();
// 显示所有私密标签
document.querySelectorAll('.private-tag').forEach(tag => {
tag.style.display = 'block';
});
logAction('显示私密花园');
}
}
// 加载分类和链接
function loadSections() {
const container = document.getElementById('sections-container');
container.innerHTML = '';
Object.keys(categories).forEach(category => {
const section = document.createElement('div');
section.className = 'section';
const titleContainer = document.createElement('div');
titleContainer.className = 'section-title-container';
const title = document.createElement('div');
title.className = 'section-title';
title.textContent = category;
titleContainer.appendChild(title);
if (isAdmin) {
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '删除分类';
deleteBtn.className = 'delete-category-btn';
deleteBtn.style.display = 'none';
deleteBtn.onclick = () => deleteCategory(category);
titleContainer.appendChild(deleteBtn);
}
const cardContainer = document.createElement('div');
cardContainer.className = 'card-container';
cardContainer.id = category;
section.appendChild(titleContainer);
section.appendChild(cardContainer);
let privateCount = 0;
let linkCount = 0;
links.forEach(link => {
if (link.category === category) {
if (link.isPrivate) privateCount++;
linkCount++;
createCard(link, cardContainer);
}
});
if (privateCount < linkCount || isLoggedIn) {
container.appendChild(section);
}
});
logAction('加载分类和链接', { isAdmin: isAdmin, linkCount: links.length, categoryCount: Object.keys(categories).length });
}
// 创建卡片
function createCard(link, container) {
const card = document.createElement('div');
card.className = 'card';
card.setAttribute('draggable', isAdmin);
card.dataset.isPrivate = link.isPrivate;
const cardTop = document.createElement('div');
cardTop.className = 'card-top';
// 定义默认的 SVG 图标
const defaultIconSVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>' +
'<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>' +
'</svg>';
// 创建图标元素
const icon = document.createElement('img');
icon.className = 'card-icon';
// icon.src = 'https://api.iowen.cn/favicon/' + extractDomain(link.url) + '.png';
icon.src = 'https://www.faviconextractor.com/favicon/' + extractDomain(link.url);
icon.alt = 'Website Icon';
// 如果图片加载失败,使用默认的 SVG 图标
icon.onerror = function() {
const svgBlob = new Blob([defaultIconSVG], {type: 'image/svg+xml'});
const svgUrl = URL.createObjectURL(svgBlob);
this.src = svgUrl;
this.onload = () => URL.revokeObjectURL(svgUrl);
};
function extractDomain(url) {
let domain;
try {
domain = new URL(url).hostname;
} catch (e) {
domain = url;
}
return domain;
}
const title = document.createElement('div');
title.className = 'card-title';
title.textContent = link.name;
cardTop.appendChild(icon);
cardTop.appendChild(title);
const url = document.createElement('div');
url.className = 'card-url';
url.textContent = link.url;
card.appendChild(cardTop);
card.appendChild(url);
if (link.isPrivate) {
const privateTag = document.createElement('div');
privateTag.className = 'private-tag';
privateTag.textContent = '私密';
card.appendChild(privateTag);
}
const correctedUrl = link.url.startsWith('http://') || link.url.startsWith('https://') ? link.url : 'http://' + link.url;
if (!isAdmin) {
card.addEventListener('click', () => {
window.open(correctedUrl, '_blank');
logAction('打开链接', { name: link.name, url: correctedUrl });
});
}
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '–';
deleteBtn.className = 'delete-btn';
deleteBtn.onclick = function (event) {
event.stopPropagation();
removeCard(card);
};
card.appendChild(deleteBtn);
updateCardStyle(card);
card.addEventListener('dragstart', dragStart);
card.addEventListener('dragover', dragOver);
card.addEventListener('dragend', dragEnd);
card.addEventListener('drop', drop);
card.addEventListener('touchstart', touchStart, { passive: false });
if (isAdmin && removeMode) {
deleteBtn.style.display = 'block';
}
if (isAdmin || (link.isPrivate && isLoggedIn) || !link.isPrivate) {
container.appendChild(card);
}
// logAction('创建卡片', { name: link.name, isPrivate: link.isPrivate });
}
// 更新卡片样式
function updateCardStyle(card) {
if (isDarkTheme) {
card.style.backgroundColor = '#1e1e1e';
card.style.color = '#ffffff';
card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)';
} else {
card.style.backgroundColor = 'rgba(160, 201, 229, 0.6)';
card.style.color = '#333';
card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)';
}
}
// 更新分类选择下拉框
function updateCategorySelect() {
const categorySelect = document.getElementById('category-select');
categorySelect.innerHTML = '';
Object.keys(categories).forEach(category => {
const option = document.createElement('option');
option.value = category;
option.textContent = category;
categorySelect.appendChild(option);
});
logAction('更新分类选择', { categoryCount: Object.keys(categories).length });
}
// 保存链接数据
async function saveLinks() {
if (isAdmin && !(await validateToken())) {
return;
}
let allLinks = [...publicLinks, ...privateLinks];
try {
await fetch('/api/saveOrder', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': localStorage.getItem('authToken')
},
body: JSON.stringify({
userId: 'testUser',
links: allLinks,
categories: categories
}),
});
logAction('保存链接', { linkCount: allLinks.length, categoryCount: Object.keys(categories).length });
} catch (error) {
logAction('保存链接失败', { error: error.message });
alert('保存链接失败,请重试');
}
}
// 添加卡片弹窗
async function addLink() {
if (!await validateToken()) {
return;
}
const name = document.getElementById('name-input').value;
const url = document.getElementById('url-input').value;
const category = document.getElementById('category-select').value;
const isPrivate = document.getElementById('private-checkbox').checked;
if (name && url && category) {
const newLink = { name, url, category, isPrivate };
if (isPrivate) {
privateLinks.push(newLink);
} else {
publicLinks.push(newLink);
}
links = isLoggedIn ? [...publicLinks, ...privateLinks] : publicLinks;
if (isAdmin || (isPrivate && isLoggedIn) || !isPrivate) {
const container = document.getElementById(category);
if (container) {
createCard(newLink, container);
} else {
categories[category] = [];
renderCategories();
}
}
saveLinks();
document.getElementById('name-input').value = '';
document.getElementById('url-input').value = '';
document.getElementById('private-checkbox').checked = false;
hideAddDialog();
logAction('添加卡片', { name, url, category, isPrivate });
}
}
// 删除卡片
async function removeCard(card) {
if (!await validateToken()) {
return;
}
const name = card.querySelector('.card-title').textContent;
const url = card.querySelector('.card-url').textContent;
const isPrivate = card.dataset.isPrivate === 'true';
links = links.filter(link => link.url !== url);
if (isPrivate) {
privateLinks = privateLinks.filter(link => link.url !== url);
} else {
publicLinks = publicLinks.filter(link => link.url !== url);
}
for (const key in categories) {
categories[key] = categories[key].filter(link => link.url !== url);
}
card.remove();
saveLinks();
logAction('删除卡片', { name, url, isPrivate });
}
// 拖拽卡片
let draggedCard = null;
let touchStartX, touchStartY;
// 触屏端拖拽卡片
function touchStart(event) {
if (!isAdmin) {
return;
}
draggedCard = event.target.closest('.card');
if (!draggedCard) return;
event.preventDefault();
const touch = event.touches[0];
touchStartX = touch.clientX;
touchStartY = touch.clientY;
draggedCard.classList.add('dragging');
document.addEventListener('touchmove', touchMove, { passive: false });
document.addEventListener('touchend', touchEnd);
}
function touchMove(event) {
if (!draggedCard) return;
event.preventDefault();
const touch = event.touches[0];
const currentX = touch.clientX;
const currentY = touch.clientY;
const deltaX = currentX - touchStartX;
const deltaY = currentY - touchStartY;
draggedCard.style.transform = "translate(" + deltaX + "px, " + deltaY + "px)";
const target = findCardUnderTouch(currentX, currentY);
if (target && target !== draggedCard) {
const container = target.parentElement;
const targetRect = target.getBoundingClientRect();
if (currentX < targetRect.left + targetRect.width / 2) {
container.insertBefore(draggedCard, target);
} else {
container.insertBefore(draggedCard, target.nextSibling);
}
}
}
function touchEnd(event) {
if (!draggedCard) return;
const card = draggedCard;
const targetCategory = card.closest('.card-container').id;
validateToken().then(isValid => {
if (isValid && card) {
updateCardCategory(card, targetCategory);
saveCardOrder().catch(error => {
console.error('Save failed:', error);
});
}
cleanupDragState();
});
}
function findCardUnderTouch(x, y) {
const cards = document.querySelectorAll('.card:not(.dragging)');
return Array.from(cards).find(card => {
const rect = card.getBoundingClientRect();
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
});
}
// PC端拖拽卡片
function dragStart(event) {
if (!isAdmin) {
event.preventDefault();
return;
}
draggedCard = event.target.closest('.card');
if (!draggedCard) return;
draggedCard.classList.add('dragging');
event.dataTransfer.effectAllowed = "move";
logAction('开始拖拽卡片', { name: draggedCard.querySelector('.card-title').textContent });
}
function dragOver(event) {
if (!isAdmin) {
event.preventDefault();
return;
}
event.preventDefault();
const target = event.target.closest('.card');
if (target && target !== draggedCard) {
const container = target.parentElement;
const mousePositionX = event.clientX;
const targetRect = target.getBoundingClientRect();
if (mousePositionX < targetRect.left + targetRect.width / 2) {
container.insertBefore(draggedCard, target);
} else {
container.insertBefore(draggedCard, target.nextSibling);
}
}
}
// 清理拖拽状态函数
function cleanupDragState() {
if (draggedCard) {
draggedCard.classList.remove('dragging');
draggedCard.style.transform = '';
draggedCard = null;
}
document.removeEventListener('touchmove', touchMove);
document.removeEventListener('touchend', touchEnd);
touchStartX = null;
touchStartY = null;
}
// PC端拖拽结束
function drop(event) {
if (!isAdmin) {
event.preventDefault();
return;
}
event.preventDefault();
const card = draggedCard;
const targetCategory = event.target.closest('.card-container').id;
validateToken().then(isValid => {
if (isValid && card) {
updateCardCategory(card, targetCategory);
saveCardOrder().catch(error => {
console.error('Save failed:', error);
});
}
cleanupDragState();
});
}
function dragEnd(event) {
if (draggedCard) {
draggedCard.classList.remove('dragging');
logAction('拖拽卡片结束');
}
}
// 更新卡片分类
function updateCardCategory(card, newCategory) {
const cardTitle = card.querySelector('.card-title').textContent;
const cardUrl = card.querySelector('.card-url').textContent;
const isPrivate = card.dataset.isPrivate === 'true';
const linkIndex = links.findIndex(link => link.url === cardUrl);
if (linkIndex !== -1) {
links[linkIndex].category = newCategory;
}
const linkArray = isPrivate ? privateLinks : publicLinks;
const arrayIndex = linkArray.findIndex(link => link.url === cardUrl);
if (arrayIndex !== -1) {
linkArray[arrayIndex].category = newCategory;
}
card.dataset.category = newCategory;
}
// 在页面加载完成后添加触摸事件监听器
document.addEventListener('DOMContentLoaded', function() {
const cardContainers = document.querySelectorAll('.card-container');
cardContainers.forEach(container => {
container.addEventListener('touchstart', touchStart, { passive: false });
});
});
// 保存卡片顺序
async function saveCardOrder() {
if (!await validateToken()) {
return;
}
const containers = document.querySelectorAll('.card-container');
let newPublicLinks = [];
let newPrivateLinks = [];
let newCategories = {};
containers.forEach(container => {
const category = container.id;
newCategories[category] = [];
[...container.children].forEach(card => {
const url = card.querySelector('.card-url').textContent;
const name = card.querySelector('.card-title').textContent;
const isPrivate = card.dataset.isPrivate === 'true';
card.dataset.category = category;
const link = { name, url, category, isPrivate };
if (isPrivate) {
newPrivateLinks.push(link);
} else {
newPublicLinks.push(link);
}
newCategories[category].push(link);
});
});
publicLinks.length = 0;
publicLinks.push(...newPublicLinks);
privateLinks.length = 0;
privateLinks.push(...newPrivateLinks);
Object.keys(categories).forEach(key => delete categories[key]);
Object.assign(categories, newCategories);
logAction('保存卡片顺序', {
publicCount: newPublicLinks.length,
privateCount: newPrivateLinks.length,
categoryCount: Object.keys(newCategories).length
});
try {
const response = await fetch('/api/saveOrder', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': localStorage.getItem('authToken')
},
body: JSON.stringify({
userId: 'testUser',
links: [...newPublicLinks, ...newPrivateLinks],
categories: newCategories
}),
});
const result = await response.json();
if (!result.success) {
throw new Error('Failed to save order');
}
logAction('保存卡片顺序', { publicCount: newPublicLinks.length, privateCount: newPrivateLinks.length, categoryCount: Object.keys(newCategories).length });
} catch (error) {
logAction('保存顺序失败', { error: error.message });
alert('保存顺序失败,请重试');
}
}
// 设置状态重新加载卡片
function reloadCardsAsAdmin() {
document.querySelectorAll('.card-container').forEach(container => {
container.innerHTML = '';
});
loadLinks().then(() => {
if (isDarkTheme) {
applyDarkTheme();
}
});
logAction('重新加载卡片(管理员模式)');
}
// 密码输入框回车事件
document.getElementById('admin-password').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
toggleSecretGarden();
}
});
// 切换设置状态
async function toggleAdminMode() {
const adminBtn = document.getElementById('admin-mode-btn');
const addRemoveControls = document.querySelector('.add-remove-controls');
if (!isAdmin && isLoggedIn) {
if (!await validateToken()) {
return;
}
// 在进入设置模式之前进行备份
try {
const response = await fetch('/api/backupData', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': localStorage.getItem('authToken')
},
body: JSON.stringify({
sourceUserId: 'testUser',
backupUserId: 'backup'
}),
});
const result = await response.json();
if (result.success) {
logAction('数据备份成功');
} else {
throw new Error('备份失败');
}
} catch (error) {
logAction('数据备份失败', { error: error.message });
if (!confirm('备份失败,是否仍要继续进入设置模式?')) {
return;
}
}
isAdmin = true;
adminBtn.textContent = "退出设置";
addRemoveControls.style.display = 'flex';
alert('准备设置分类和书签');
reloadCardsAsAdmin();
logAction('进入设置');
} else if (isAdmin) {
isAdmin = false;
removeMode = false;
adminBtn.textContent = "设 置";
addRemoveControls.style.display = 'none';
alert('设置已保存');
reloadCardsAsAdmin();
logAction('离开设置');
}
updateUIState();
}
// 切换到登录状态
function toggleSecretGarden() {
const passwordInput = document.getElementById('admin-password');
if (!isLoggedIn) {
verifyPassword(passwordInput.value)
.then(result => {
if (result.valid) {
isLoggedIn = true;
localStorage.setItem('authToken', result.token);
console.log('Token saved:', result.token);
loadLinks();
alert('登录成功!');
logAction('登录成功');
} else {
alert('密码错误');
logAction('登录失败', { reason: result.error || '密码错误' });
}
updateUIState();
})
.catch(error => {
console.error('Login error:', error);
alert('登录过程出错,请重试');
});
} else {
isLoggedIn = false;
isAdmin = false;
localStorage.removeItem('authToken');
links = publicLinks;
loadSections();
alert('退出登录!');
updateUIState();
passwordInput.value = '';
logAction('退出登录');
}
}
// 应用暗色主题
function applyDarkTheme() {
document.body.style.backgroundColor = '#121212';
document.body.style.color = '#ffffff';
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
card.style.backgroundColor = '#1e1e1e';
card.style.color = '#ffffff';
card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)';
});
logAction('应用暗色主题');
}
// 显示添加链接对话框
function showAddDialog() {
document.getElementById('dialog-overlay').style.display = 'flex';
logAction('显示添加链接对话框');
}
// 隐藏添加链接对话框
function hideAddDialog() {
document.getElementById('dialog-overlay').style.display = 'none';
logAction('隐藏添加链接对话框');
}
// 切换删除卡片模式
function toggleRemoveMode() {
removeMode = !removeMode;
const deleteButtons = document.querySelectorAll('.delete-btn');
deleteButtons.forEach(btn => {
btn.style.display = removeMode ? 'block' : 'none';
});
logAction('切换删除卡片模式', { removeMode });
}
//切换删除分类模式
function toggleRemoveCategory() {
isRemoveCategoryMode = !isRemoveCategoryMode;
const deleteButtons = document.querySelectorAll('.delete-category-btn');
deleteButtons.forEach(btn => {
btn.style.display = isRemoveCategoryMode ? 'inline-block' : 'none';
});
logAction('切换删除分类模式', { isRemoveCategoryMode });
}
// 切换主题
function toggleTheme() {
isDarkTheme = !isDarkTheme;
document.body.style.backgroundColor = isDarkTheme ? '#121212' : '#ffffff';
document.body.style.color = isDarkTheme ? '#ffffff' : '#333';
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
card.style.backgroundColor = isDarkTheme ? '#1e1e1e' : 'rgba(160, 201, 229, 0.6)';
card.style.color = isDarkTheme ? '#ffffff' : '#333';
card.style.boxShadow = isDarkTheme
? '0 4px 8px rgba(0, 0, 0, 0.5)'
: '0 4px 8px rgba(0, 0, 0, 0.1)';
});
const fixedElements = document.querySelectorAll('.fixed-elements');
fixedElements.forEach(element => {
element.style.backgroundColor = isDarkTheme ? '#121212' : '#ffffff';
element.style.color = isDarkTheme ? '#ffffff' : '#333';
});
const dialogBox = document.getElementById('dialog-box');
dialogBox.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#ffffff';
dialogBox.style.color = isDarkTheme ? '#ffffff' : '#333';
const inputs = document.querySelectorAll('input[type="text"], input[type="password"], select');
inputs.forEach(input => {
input.style.backgroundColor = isDarkTheme ? '#444' : '#fff';
input.style.color = isDarkTheme ? '#fff' : '#333';
input.style.borderColor = isDarkTheme ? '#555' : '#ccc';
});
logAction('切换主题', { isDarkTheme });
}
// 验证密码
async function verifyPassword(inputPassword) {
const response = await fetch('/api/verifyPassword', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: inputPassword }),
});
const result = await response.json();
return result;
}
// 初始化加载
document.addEventListener('DOMContentLoaded', async () => {
await validateToken();
loadLinks();
});
// 前端检查是否有 token
async function validateToken() {
const token = localStorage.getItem('authToken');
if (!token) {
isLoggedIn = false;
updateUIState();
return false;
}
try {
const response = await fetch('/api/getLinks?userId=testUser', {
headers: { 'Authorization': token }
});
if (response.status === 401) {
await resetToLoginState('token已过期,请重新登录');
return false;
}
isLoggedIn = true;
updateUIState();
return true;
} catch (error) {
console.error('Token validation error:', error);
return false;
}
}
// 重置状态
async function resetToLoginState(message) {
alert(message);
cleanupDragState();
localStorage.removeItem('authToken');
isLoggedIn = false;
isAdmin = false;
removeMode = false;
isRemoveCategoryMode = false;
const passwordInput = document.getElementById('admin-password');
if (passwordInput) {
passwordInput.value = '';
}
updateUIState();
links = publicLinks;
loadSections();
const addRemoveControls = document.querySelector('.add-remove-controls');
if (addRemoveControls) {
addRemoveControls.style.display = 'none';
}
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.style.display = 'none';
});
document.querySelectorAll('.delete-category-btn').forEach(btn => {
btn.style.display = 'none';
});
const dialogOverlay = document.getElementById('dialog-overlay');
if (dialogOverlay) {
dialogOverlay.style.display = 'none';
}
}
</script>
</body>
</html>
`;
// 服务端 token 验证
async function validateServerToken(authToken, env) {
if (!authToken) {
return {
isValid: false,
status: 401,
response: { error: 'Unauthorized', message: '未登录或登录已过期' }
};
}
try {
const [timestamp, hash] = authToken.split('.');
const tokenTimestamp = parseInt(timestamp);
const now = Date.now();
const FIFTEEN_MINUTES = 15 * 60 * 1000;
if (now - tokenTimestamp > FIFTEEN_MINUTES) {
return {
isValid: false,
status: 401,
response: {
error: 'Token expired',
tokenExpired: true,
message: '登录已过期,请重新登录'
}
};
}
const tokenData = timestamp + "_" + env.ADMIN_PASSWORD;
const encoder = new TextEncoder();
const data = encoder.encode(tokenData);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const expectedHash = btoa(String.fromCharCode(...new Uint8Array(hashBuffer)));
if (hash !== expectedHash) {
return {
isValid: false,
status: 401,
response: {
error: 'Invalid token',
tokenInvalid: true,
message: '登录状态无效,请重新登录'
}
};
}
return { isValid: true };
} catch (error) {
return {
isValid: false,
status: 401,
response: {
error: 'Invalid token',
tokenInvalid: true,
message: '登录验证失败,请重新登录'
}
};
}
}
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === '/') {
return new Response(HTML_CONTENT, {
headers: { 'Content-Type': 'text/html' }
});
}
if (url.pathname === '/api/getLinks') {
const userId = url.searchParams.get('userId');
const authToken = request.headers.get('Authorization');
const data = await env.CARD_ORDER.get(userId);
if (data) {
const parsedData = JSON.parse(data);
// 验证 token
if (authToken) {
const validation = await validateServerToken(authToken, env);
if (!validation.isValid) {
return new Response(JSON.stringify(validation.response), {
status: validation.status,
headers: { 'Content-Type': 'application/json' }
});
}
// Token 有效,返回完整数据
return new Response(JSON.stringify(parsedData), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
// 未提供 token,只返回公开数据
const filteredLinks = parsedData.links.filter(link => !link.isPrivate);
const filteredCategories = {};
Object.keys(parsedData.categories).forEach(category => {
filteredCategories[category] = parsedData.categories[category].filter(link => !link.isPrivate);
});
return new Response(JSON.stringify({
links: filteredLinks,
categories: filteredCategories
}), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
return new Response(JSON.stringify({
links: [],
categories: {}
}), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
if (url.pathname === '/api/saveOrder' && request.method === 'POST') {
const authToken = request.headers.get('Authorization');
const validation = await validateServerToken(authToken, env);
if (!validation.isValid) {
return new Response(JSON.stringify(validation.response), {
status: validation.status,
headers: { 'Content-Type': 'application/json' }
});
}
const { userId, links, categories } = await request.json();
await env.CARD_ORDER.put(userId, JSON.stringify({ links, categories }));
return new Response(JSON.stringify({
success: true,
message: '保存成功'
}), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
if (url.pathname === '/api/verifyPassword' && request.method === 'POST') {
try {
const { password } = await request.json();
const isValid = password === env.ADMIN_PASSWORD;
if (isValid) {
// 生成包含时间戳的加密 token
const timestamp = Date.now();
const tokenData = timestamp + "_" + env.ADMIN_PASSWORD;
const encoder = new TextEncoder();
const data = encoder.encode(tokenData);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
// 使用指定格式:timestamp.hash
const token = timestamp + "." + btoa(String.fromCharCode(...new Uint8Array(hashBuffer)));
return new Response(JSON.stringify({
valid: true,
token: token
}), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
return new Response(JSON.stringify({
valid: false,
error: 'Invalid password'
}), {
status: 403,
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
return new Response(JSON.stringify({
valid: false,
error: error.message
}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
}
if (url.pathname === '/api/backupData' && request.method === 'POST') {
const { sourceUserId } = await request.json();
const result = await this.backupData(env, sourceUserId);
return new Response(JSON.stringify(result), {
status: result.success ? 200 : 404,
headers: { 'Content-Type': 'application/json' }
});
}
return new Response('Not Found', { status: 404 });
},
async backupData(env, sourceUserId) {
const MAX_BACKUPS = 10;
const sourceData = await env.CARD_ORDER.get(sourceUserId);
if (sourceData) {
try {
const currentDate = new Date().toLocaleString('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}).replace(/\//g, '-');
const backupId = `backup_${currentDate}`;
const backups = await env.CARD_ORDER.list({ prefix: 'backup_' });
const backupKeys = backups.keys.map(key => key.name).sort((a, b) => {
const timeA = new Date(a.split('_')[1].replace(/-/g, '/')).getTime();
const timeB = new Date(b.split('_')[1].replace(/-/g, '/')).getTime();
return timeB - timeA; // 降序排序,最新的在前
});
await env.CARD_ORDER.put(backupId, sourceData);
const allBackups = [...backupKeys, backupId].sort((a, b) => {
const timeA = new Date(a.split('_')[1].replace(/-/g, '/')).getTime();
const timeB = new Date(b.split('_')[1].replace(/-/g, '/')).getTime();
return timeB - timeA;
});
const backupsToDelete = allBackups.slice(MAX_BACKUPS);
if (backupsToDelete.length > 0) {
await Promise.all(
backupsToDelete.map(key => env.CARD_ORDER.delete(key))
);
}
return {
success: true,
backupId,
remainingBackups: MAX_BACKUPS,
deletedCount: backupsToDelete.length
};
} catch (error) {
return {
success: false,
error: 'Backup operation failed',
details: error.message
};
}
}
return { success: false, error: 'Source data not found' };
}
};
2024.10.26 修复:
原获取网站图标的接口https://favicon.zhusl.com/ico?url= 好像失效了,现更换接口https://www.faviconextractor.com/favicon/ 项目地址:【Github】 ;另一个备份接口为https://api.iowen.cn/favicon 项目地址:【Github】 感谢上述接口作者提供的服务
const HTML_CONTENT = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Card Tab</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2280%22>💠</text></svg>">
<style>
/* 全局样式 */
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #d4d4d4;
transition: background-color 0.3s ease;
}
ul {
padding: 0;
margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 0px;
margin-inline-end: 0px;
padding-inline-start: 40px;
unicode-bidi: isolate;
}
li {
display: list-item;
text-align: -webkit-match-parent;
unicode-bidi: isolate;
margin: 0 8px;
}
.background {
background-image: linear-gradient(#d4d4d4 1px, transparent 0), linear-gradient(90deg, #d4d4d4 1px, transparent 0);
background-size: 32px 32px;
background-color: #fffcf8;
}
.header {
background-color: #fff;
box-shadow: 0 0 5px rgba(0, 0, 0, .1);
transition: background-color .5s;
}
.navbar {
display: flex;
width: 1280px;
margin: auto;
height: 40px;
}
.navbar .brand {
display: flex;
align-items: center;
color: #555;
}
.brand .logo {
max-width: 36px;
}
.brand .title {
margin-left: 5px;
font-family: helvetica neue, helvetica, arial, sans-serif;
font-size: 24px;
font-weight: 700;
}
.beta {
color: #ccc;
font-size: 11px;
font-weight: 400;
position: relative;
top: -14px;
}
.category-list {
list-style: none;
display: flex;
align-items: center;
}
.sites {
width:1286px;
background:;
border:2px solid auto;
margin:15px auto;
padding:0px;
text-align:center;
}
.sites1 {
width:1286px;
background:;
border:2px solid auto;
margin:15px auto;
padding:0px;
}
.sites dl {
height:36px;
line-height:36px;
display:block;
margin:0;
}
.sites dl.alt {
background:#d4d4d4;
border-top:1px solid #ffffff;
border-bottom:1px solid #ffffff;
}
.sites dl.alt2 {
background:#d4d4d4;
border-top:1px solid #ffffff;
border-bottom:1px solid #ffffff;
}
.sites dt,.sites dd {
text-align:center;
display:block;
float:left;
}
.sites dt {
width:60px;
}
.sites dd {
width:90px;
margin:0;
}
.footer {
width:580px;
text-align:center;
margin:5px auto;
padding:2px;
}
/* 固定元素样式 */
.fixed-elements {
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: #d4d4d4;
z-index: 1000;
padding: 10px;
transition: background-color 0.3s ease;
height: 130px;
}
.fixed-elements h3 {
position: absolute;
top: 10px;
left: 20px;
margin: 0;
}
/* 中心内容样式 */
.center-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
max-width: 600px;
text-align: center;
}
/* 管理员控制面板样式 */
.admin-controls {
position: fixed;
top: 10px;
right: 10px;
font-size: 60%;
}
/* 添加/删除控制按钮样式 */
.add-remove-controls {
display: none;
flex-direction: column;
position: fixed;
right: 20px;
top: 50%;
transform: translateY(-50%);
align-items: center;
gap: 10px;
}
.round-btn {
background-color: #007bff;
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
text-align: center;
font-size: 24px;
line-height: 40px;
cursor: pointer;
margin: 5px 0;
}
.add-btn { order: 1; }
.remove-btn { order: 2; }
.category-btn { order: 3; }
.remove-category-btn { order: 4; }
/* 主要内容区域样式 */
.content {
margin-top: 140px;
padding: 20px;
}
/* 搜索栏样式 */
.search-container {
margin-top: 10px;
}
.search-bar {
display: flex;
justify-content: center;
margin-bottom: 10px;
}
.search-bar input {
width: 50%;
padding: 5px;
border: 1px solid #ccc;
border-radius: 5px 0 0 5px;
}
.search-bar button {
padding: 5px 10px;
border: 1px solid #ccc;
border-left: none;
background-color: #a0c9e5;
border-radius: 0 5px 5px 0;
cursor: pointer;
}
/* 搜索引擎按钮样式 */
.search-engines {
display: flex;
justify-content: center;
gap: 10px;
}
.search-engine {
padding: 5px 10px;
border: 1px solid #ccc;
background-color: #f0f0;
border-radius: 5px;
cursor: pointer;
}
/* 主题切换按钮样式 */
#theme-toggle {
position: fixed;
bottom: 50px;
right: 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
text-align: center;
font-size: 24px;
line-height: 40px;
cursor: pointer;
}
/* 显示日志按钮样式 */
#view-logs-btn {
position: fixed;
top: 100px;
right: 10px;
z-index: 1000;
}
/* 对话框样式 */
#dialog-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
}
#dialog-box {
background-color: white;
padding: 20px;
border-radius: 5px;
width: 300px;
}
#dialog-box input, #dialog-box select {
width: 100%;
margin-bottom: 10px;
padding: 5px;
}
/* 分类和卡片样式 */
.section {
margin-bottom: 20px;
}
.section-title-container {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.section-title {
font-size: 18px;
font-weight: bold;
}
.delete-category-btn {
background-color: pink;
color: white;
border: none;
padding: 5px 10px;
border-radius: 5px;
cursor: pointer;
}
.card-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.card {
background-color: #a0c9e5;
border-radius: 5px;
padding: 10px;
width: 132px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s;
position: relative;
user-select: none;
}
/* 中等屏幕 (平板等) */
@media (max-width: 1024px) {
}
/* 小屏幕 (手机) */
@media (max-width: 768px) {
}
/* 超小屏幕 (更小手机) */
@media (max-width: 480px) {
.card {
padding: 6px; /* 进一步减小卡片内边距6 */
width: 235px;
}
}
.card:hover {
transform: translateY(-5px);
}
.card-top {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.card-icon {
width: 20px;
height: 20px;
margin-right: 5px;
}
.card-title {
font-size: 18px;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-url {
font-size: 12px;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.private-tag {
background-color: #ff9800;
color: white;
font-size: 10px;
padding: 2px 5px;
border-radius: 3px;
position: absolute;
top: 5px;
right: 5px;
}
.delete-btn {
position: absolute;
top: -10px;
right: -10px;
background-color: red;
color: white;
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
text-align: center;
font-size: 14px;
line-height: 20px;
cursor: pointer;
display: none;
}
/* 版权信息样式 */
#copyright {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 40px;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
z-index: 1000;
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
}
#copyright p {
margin: 0;
}
#copyright a {
color: #007bff;
text-decoration: none;
}
#copyright a:hover {
text-decoration: underline;
}
/* 响应式设计 */
@media (max-width: 480px) {
.fixed-elements {
position: relative;
padding: 5px;
}
.content {
margin-top: 10px;
}
.admin-controls input,
.admin-controls button {
height: 30%;
}
.card-container {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.card {
width: 80%;
max-width: 100%;
padding: 5px;
}
.card-title {
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 130px;
}
.card-url {
font-size: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 130px;
}
.add-remove-controls {
right: 2px;
}
.round-btn,
#theme-toggle {
right: 5px;
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
font-size: 24px;
}
}
</style>
</head>
<body class="background">
</div>
<div class="sites"> <!-- 00 -->
<header class="header">
<nav class="navbar">
<a href="https://www.199881.xyz/" class="brand">
<img class="debug logo" src="https://cdn.glitch.global/efdace30-a873-49c7-aaa9-4fa31679ee0c/thumbnails%2F%E5%9B%BE%E6%A0%8701.jpg?1692046715299">
<span class="debug title">WS01の主页</span>
<span class="debug beta">beta</span>
<!-- 一言模块 -->
<p id="hitokoto">
<a href="#" id="hitokoto_text"></a>
</p>
<script src="https://v1.hitokoto.cn/?encode=js&select=%23hitokoto" defer></script>
</a>
</header>
<!-- 搜索栏 -->
<div class="search-container">
<div class="search-bar">
<input type="text" id="search-input" placeholder="">
<button id="search-button">🔍</button>
</div>
<div class="search-engines">
<button class="search-engine" data-engine="baidu">百度</button>
<button class="search-engine" data-engine="bing">必应</button>
<button class="search-engine" data-engine="toutiao">头条</button>
<button class="search-engine" data-engine="sm">神马</button>
<button class="search-engine" data-engine="bilibili">哔哩</button>
<button class="search-engine" data-engine="github">github</button>
<button class="search-engine" data-engine="google">谷歌</button>
<button class="search-engine" data-engine="yandex">yandex</button>
</div>
</div>
<div class="head">
<!-- 添加/删除控制按钮 -->
<div class="add-remove-controls">
<button class="round-btn add-btn" onclick="showAddDialog()">+</button>
<button class="round-btn remove-btn" onclick="toggleRemoveMode()">-</button>
<button class="round-btn category-btn" onclick="addCategory()">C+</button>
<button class="round-btn remove-category-btn" onclick="toggleRemoveCategory()">C-</button>
</div>
<div class="sites1"> <!-- 00 -->
<!-- 分类和卡片容器 -->
<div id="sections-container"></div>
<!-- 主题切换按钮 -->
<button id="theme-toggle" onclick="toggleTheme()">◑</button>
<!-- 显示日志按钮 用于调试-->
<!--<button id="view-logs-btn" onclick="viewLogs()">显示日志</button>-->
<!-- 添加链接对话框 -->
<div id="dialog-overlay">
<div id="dialog-box">
<label for="name-input">名称</label>
<input type="text" id="name-input">
<label for="url-input">地址</label>
<input type="text" id="url-input">
<label for="category-select">选择分类</label>
<select id="category-select"></select>
<div class="private-link-container">
<label for="private-checkbox">私密链接</label>
<input type="checkbox" id="private-checkbox">
</div>
<button onclick="addLink()">确定</button>
<button onclick="hideAddDialog()">取消</button>
</div>
</div>
<br />
<!-- body 页脚 -->
<div class="footer">
<!-- 管理员控制面板 -->
<input type="password" id="admin-password" placeholder="输入密码">
<button id="admin-mode-btn" onclick="toggleAdminMode()">设 置</button>
<button id="secret-garden-btn" onclick="toggleSecretGarden()">登 录</button>
<br />
<br /> <br />
<!-- 版权信息 -->
<div id="copyright" class="copyright">
<!--请不要删除-->
<p><a href="https://github.com/hmhm2022/Card-Tab" target="_blank">GitHub</a> 项目
<!-- 开站时间开始 -->
<span id="timeDate">载入天数...</span><span id="times">载入时分秒...</span> <script language="javascript">
var now = new Date();
function createtime(){
var grt= new Date("09/05/2024 00:00:00");/*---这里是网站的启用时间--*/
now.setTime(now.getTime()+250);
days = (now - grt ) / 1000 / 60 / 60 / 24;
dnum = Math.floor(days);
hours = (now - grt ) / 1000 / 60 / 60 - (24 * dnum);
hnum = Math.floor(hours);
if(String(hnum).length ==1 ){hnum = "0" + hnum;}
minutes = (now - grt ) / 1000 /60 - (24 * 60 * dnum) - (60 * hnum);
mnum = Math.floor(minutes);
if(String(mnum).length ==1 ){mnum = "0" + mnum;}
seconds = (now - grt ) / 1000 - (24 * 60 * 60 * dnum) - (60 * 60 * hnum) - (60 * mnum);
snum = Math.round(seconds);
if(String(snum).length ==1 ){snum = "0" + snum;}
document.getElementById("timeDate").innerHTML = "稳定运行"+dnum+"天";
document.getElementById("times").innerHTML = hnum + "小时" + mnum + "分" + snum + "秒";
}
setInterval("createtime()",250);
</script>
<!-- 开站时间结束 -->
<!-- <script defer src="https://four-root-occupation.glitch.me/denglong.js"></script> 国庆快乐-->
</p>
</div>
</div>
<script>
// 搜索引擎配置
const searchEngines = {
baidu: "https://www.baidu.com/s?wd=",
bing: "https://www.bing.com/search?q=",
sm: "https://m.sm.cn/s?q=",
toutiao: "https://so.toutiao.com/search?dvpf=pc&source=trending_card&keyword=",
bilibili: "https://search.bilibili.com/all?keyword=",
github: "https://github.com/search?q=",
google: "https://www.google.com/search?q=",
yandex: "https://www.yandex.com/search/?text="
};
let currentEngine = "baidu";
// 日志记录函数
function logAction(action, details) {
const timestamp = new Date().toISOString();
const logEntry = timestamp + ': ' + action + ' - ' + JSON.stringify(details);
let logs = JSON.parse(localStorage.getItem('cardTabLogs') || '[]');
logs.push(logEntry);
// 保留最新的1000条日志
if (logs.length > 1000) {
logs = logs.slice(-1000);
}
localStorage.setItem('cardTabLogs', JSON.stringify(logs));
console.log(logEntry); // 同时在控制台输出日志
}
// 查看日志的函数
function viewLogs() {
const logs = JSON.parse(localStorage.getItem('cardTabLogs') || '[]');
console.log('Card Tab Logs:');
logs.forEach(log => console.log(log));
alert('日志已在控制台输出,请按F12打开开发者工具查看。');
}
// 设置当前搜索引擎
function setActiveEngine(engine) {
currentEngine = engine;
document.querySelectorAll('.search-engine').forEach(btn => {
btn.style.backgroundColor = btn.dataset.engine === engine ? '#c0c0c0' : '#f0f0f0';
});
logAction('设置搜索引擎', { engine });
}
// 搜索引擎按钮点击事件
document.querySelectorAll('.search-engine').forEach(button => {
button.addEventListener('click', () => setActiveEngine(button.dataset.engine));
});
// 搜索按钮点击事件
document.getElementById('search-button').addEventListener('click', () => {
const query = document.getElementById('search-input').value;
if (query) {
logAction('执行搜索', { engine: currentEngine, query });
window.open(searchEngines[currentEngine] + encodeURIComponent(query), '_blank');
}
});
// 搜索输入框回车事件
document.getElementById('search-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
document.getElementById('search-button').click();
}
});
// 初始化搜索引擎
setActiveEngine(currentEngine);
// 全局变量
let publicLinks = [];
let privateLinks = [];
let isAdmin = false;
let isLoggedIn = false;
let removeMode = false;
let isRemoveCategoryMode = false;
let isDarkTheme = false;
let links = [];
const categories = {};
// 添加新分类
function addCategory() {
const categoryName = prompt('请输入新分类名称:');
if (categoryName && !categories[categoryName]) {
categories[categoryName] = [];
updateCategorySelect();
renderCategories();
saveLinks();
logAction('添加分类', { categoryName, currentLinkCount: links.length });
} else if (categories[categoryName]) {
alert('该分类已存在');
logAction('添加分类失败', { categoryName, reason: '分类已存在' });
}
}
// 删除分类
function deleteCategory(category) {
if (confirm('确定要删除 "' + category + '" 分类吗?这将删除该分类下的所有链接。')) {
delete categories[category];
links = links.filter(link => link.category !== category);
publicLinks = publicLinks.filter(link => link.category !== category);
privateLinks = privateLinks.filter(link => link.category !== category);
updateCategorySelect();
saveLinks();
renderCategories();
logAction('删除分类', { category });
}
}
// 渲染分类(不重新加载链接)
function renderCategories() {
const container = document.getElementById('sections-container');
container.innerHTML = '';
Object.keys(categories).forEach(category => {
const section = document.createElement('div');
section.className = 'section';
const titleContainer = document.createElement('div');
titleContainer.className = 'section-title-container';
const title = document.createElement('div');
title.className = 'section-title';
title.textContent = category;
titleContainer.appendChild(title);
if (isAdmin) {
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '删除分类';
deleteBtn.className = 'delete-category-btn';
deleteBtn.style.display = isRemoveCategoryMode ? 'inline-block' : 'none';
deleteBtn.onclick = () => deleteCategory(category);
titleContainer.appendChild(deleteBtn);
}
const cardContainer = document.createElement('div');
cardContainer.className = 'card-container';
cardContainer.id = category;
section.appendChild(titleContainer);
section.appendChild(cardContainer);
container.appendChild(section);
const categoryLinks = links.filter(link => link.category === category);
categoryLinks.forEach(link => {
createCard(link, cardContainer);
});
});
logAction('渲染分类', { categoryCount: Object.keys(categories).length, linkCount: links.length });
}
// 读取链接数据
async function loadLinks() {
const response = await fetch('/api/getLinks?userId=testUser');
const data = await response.json();
if (data.categories) {
Object.assign(categories, data.categories);
}
publicLinks = data.links ? data.links.filter(link => !link.isPrivate) : [];
privateLinks = data.links ? data.links.filter(link => link.isPrivate) : [];
links = isLoggedIn ? [...publicLinks, ...privateLinks] : publicLinks;
loadSections();
updateCategorySelect();
updateUIState();
logAction('读取链接', { publicCount: publicLinks.length, privateCount: privateLinks.length });
}
// 更新UI状态
function updateUIState() {
const passwordInput = document.getElementById('admin-password');
const adminBtn = document.getElementById('admin-mode-btn');
const secretGardenBtn = document.getElementById('secret-garden-btn');
const addRemoveControls = document.querySelector('.add-remove-controls');
passwordInput.style.display = isLoggedIn ? 'none' : 'inline-block';
secretGardenBtn.textContent = isLoggedIn ? "退出" : "登录";
secretGardenBtn.style.display = 'inline-block';
if (isAdmin) {
adminBtn.textContent = "离开设置";
adminBtn.style.display = 'inline-block';
addRemoveControls.style.display = 'flex';
} else if (isLoggedIn) {
adminBtn.textContent = "设置";
adminBtn.style.display = 'inline-block';
addRemoveControls.style.display = 'none';
} else {
adminBtn.style.display = 'none';
addRemoveControls.style.display = 'none';
}
logAction('更新UI状态', { isAdmin, isLoggedIn });
}
// 登录状态显示(加载所有链接)
function showSecretGarden() {
if (isLoggedIn) {
links = [...publicLinks, ...privateLinks];
loadSections();
// 显示所有私密标签
document.querySelectorAll('.private-tag').forEach(tag => {
tag.style.display = 'block';
});
logAction('显示私密花园');
}
}
// 加载分类和链接
function loadSections() {
const container = document.getElementById('sections-container');
container.innerHTML = '';
Object.keys(categories).forEach(category => {
const section = document.createElement('div');
section.className = 'section';
const titleContainer = document.createElement('div');
titleContainer.className = 'section-title-container';
const title = document.createElement('div');
title.className = 'section-title';
title.textContent = category;
titleContainer.appendChild(title);
if (isAdmin) {
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '删除分类';
deleteBtn.className = 'delete-category-btn';
deleteBtn.style.display = 'none';
deleteBtn.onclick = () => deleteCategory(category);
titleContainer.appendChild(deleteBtn);
}
const cardContainer = document.createElement('div');
cardContainer.className = 'card-container';
cardContainer.id = category;
section.appendChild(titleContainer);
section.appendChild(cardContainer);
let privateCount = 0;
let linkCount = 0;
links.forEach(link => {
if (link.category === category) {
if (link.isPrivate) privateCount++;
linkCount++;
createCard(link, cardContainer);
}
});
if (privateCount < linkCount || isLoggedIn) {
container.appendChild(section);
}
});
logAction('加载分类和链接', { isAdmin: isAdmin, linkCount: links.length, categoryCount: Object.keys(categories).length });
}
// 创建卡片
function createCard(link, container) {
const card = document.createElement('div');
card.className = 'card';
card.setAttribute('draggable', isAdmin);
card.dataset.isPrivate = link.isPrivate;
const cardTop = document.createElement('div');
cardTop.className = 'card-top';
// const icon = document.createElement('img');
// icon.className = 'card-icon';
// icon.src = 'https://favicon.zhusl.com/ico?url=' + link.url;
// icon.alt = 'Website Icon';
// 定义默认的 SVG 图标
const defaultIconSVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>' +
'<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>' +
'</svg>';
// 创建图标元素
const icon = document.createElement('img');
icon.className = 'card-icon';
// icon.src = 'https://api.iowen.cn/favicon/' + extractDomain(link.url) + '.png';
icon.src = 'https://www.faviconextractor.com/favicon/' + extractDomain(link.url);
icon.alt = 'Website Icon';
// 错误处理:如果图片加载失败,使用默认的 SVG 图标
icon.onerror = function() {
const svgBlob = new Blob([defaultIconSVG], {type: 'image/svg+xml'});
const svgUrl = URL.createObjectURL(svgBlob);
this.src = svgUrl;
// 清理:当图片不再需要时,撤销对象 URL
this.onload = () => URL.revokeObjectURL(svgUrl);
};
// 辅助函数:从 URL 中提取域名
function extractDomain(url) {
let domain;
try {
domain = new URL(url).hostname;
} catch (e) {
// 如果 URL 无效,返回原始输入
domain = url;
}
return domain;
}
const title = document.createElement('div');
title.className = 'card-title';
title.textContent = link.name;
cardTop.appendChild(icon);
cardTop.appendChild(title);
const url = document.createElement('div');
url.className = 'card-url';
url.textContent = link.url;
card.appendChild(cardTop);
card.appendChild(url);
if (link.isPrivate) {
const privateTag = document.createElement('div');
privateTag.className = 'private-tag';
privateTag.textContent = '私密';
card.appendChild(privateTag);
}
const correctedUrl = link.url.startsWith('http://') || link.url.startsWith('https://') ? link.url : 'http://' + link.url;
if (!isAdmin) {
card.addEventListener('click', () => {
window.open(correctedUrl, '_blank');
logAction('打开链接', { name: link.name, url: correctedUrl });
});
}
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '–';
deleteBtn.className = 'delete-btn';
deleteBtn.onclick = function (event) {
event.stopPropagation();
removeCard(card);
};
card.appendChild(deleteBtn);
updateCardStyle(card);
card.addEventListener('dragstart', dragStart);
card.addEventListener('dragover', dragOver);
card.addEventListener('dragend', dragEnd);
card.addEventListener('drop', drop);
card.addEventListener('touchstart', touchStart, { passive: false });
if (isAdmin && removeMode) {
deleteBtn.style.display = 'block';
}
if (isAdmin || (link.isPrivate && isLoggedIn) || !link.isPrivate) {
container.appendChild(card);
}
// logAction('创建卡片', { name: link.name, isPrivate: link.isPrivate });
}
// 更新卡片样式
function updateCardStyle(card) {
if (isDarkTheme) {
card.style.backgroundColor = '#1e1e1e';
card.style.color = '#ffffff';
card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)';
} else {
card.style.backgroundColor = '#a0c9e5';
card.style.color = '#333';
card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)';
}
}
// 更新分类选择下拉框
function updateCategorySelect() {
const categorySelect = document.getElementById('category-select');
categorySelect.innerHTML = '';
Object.keys(categories).forEach(category => {
const option = document.createElement('option');
option.value = category;
option.textContent = category;
categorySelect.appendChild(option);
});
logAction('更新分类选择', { categoryCount: Object.keys(categories).length });
}
// 保存链接数据
async function saveLinks() {
let allLinks = [...publicLinks, ...privateLinks];
try {
await fetch('/api/saveOrder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: 'testUser',
links: allLinks,
categories: categories
}),
});
logAction('保存链接', { linkCount: allLinks.length, categoryCount: Object.keys(categories).length });
} catch (error) {
console.error('Error saving links:', error);
logAction('保存链接失败', { error: error.message });
alert('保存链接失败,请重试');
}
}
// 添加卡片弹窗
function addLink() {
const name = document.getElementById('name-input').value;
const url = document.getElementById('url-input').value;
const category = document.getElementById('category-select').value;
const isPrivate = document.getElementById('private-checkbox').checked;
if (name && url && category) {
const newLink = { name, url, category, isPrivate };
if (isPrivate) {
privateLinks.push(newLink);
} else {
publicLinks.push(newLink);
}
links = isLoggedIn ? [...publicLinks, ...privateLinks] : publicLinks;
if (isAdmin || (isPrivate && isLoggedIn) || !isPrivate) {
const container = document.getElementById(category);
if (container) {
createCard(newLink, container);
} else {
categories[category] = [];
renderCategories();
}
}
saveLinks();
document.getElementById('name-input').value = '';
document.getElementById('url-input').value = '';
document.getElementById('private-checkbox').checked = false;
hideAddDialog();
logAction('添加卡片', { name, url, category, isPrivate });
}
}
// 删除卡片
function removeCard(card) {
const name = card.querySelector('.card-title').textContent;
const url = card.querySelector('.card-url').textContent;
const isPrivate = card.dataset.isPrivate === 'true';
links = links.filter(link => link.url !== url);
if (isPrivate) {
privateLinks = privateLinks.filter(link => link.url !== url);
} else {
publicLinks = publicLinks.filter(link => link.url !== url);
}
for (const key in categories) {
categories[key] = categories[key].filter(link => link.url !== url);
}
card.remove();
saveLinks();
logAction('删除卡片', { name, url, isPrivate });
}
// 拖拽卡片
let draggedCard = null;
let touchStartX, touchStartY;
// 触屏端拖拽卡片
function touchStart(event) {
if (!isAdmin) return;
draggedCard = event.target.closest('.card');
if (!draggedCard) return;
event.preventDefault();
const touch = event.touches[0];
touchStartX = touch.clientX;
touchStartY = touch.clientY;
draggedCard.classList.add('dragging');
document.addEventListener('touchmove', touchMove, { passive: false });
document.addEventListener('touchend', touchEnd);
}
function touchMove(event) {
if (!draggedCard) return;
event.preventDefault();
const touch = event.touches[0];
const currentX = touch.clientX;
const currentY = touch.clientY;
const deltaX = currentX - touchStartX;
const deltaY = currentY - touchStartY;
draggedCard.style.transform = "translate(" + deltaX + "px, " + deltaY + "px)";
const target = findCardUnderTouch(currentX, currentY);
if (target && target !== draggedCard) {
const container = target.parentElement;
const targetRect = target.getBoundingClientRect();
if (currentX < targetRect.left + targetRect.width / 2) {
container.insertBefore(draggedCard, target);
} else {
container.insertBefore(draggedCard, target.nextSibling);
}
}
}
function touchEnd(event) {
if (!draggedCard) return;
document.removeEventListener('touchmove', touchMove);
document.removeEventListener('touchend', touchEnd);
draggedCard.classList.remove('dragging');
draggedCard.style.transform = '';
const targetCategory = draggedCard.closest('.card-container').id;
updateCardCategory(draggedCard, targetCategory);
saveCardOrder();
draggedCard = null;
}
function findCardUnderTouch(x, y) {
const cards = document.querySelectorAll('.card:not(.dragging)');
return Array.from(cards).find(card => {
const rect = card.getBoundingClientRect();
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
});
}
// PC端拖拽卡片
function dragStart(event) {
if (!isAdmin) return;
draggedCard = event.target.closest('.card');
if (!draggedCard) return;
draggedCard.classList.add('dragging');
event.dataTransfer.effectAllowed = "move";
logAction('开始拖拽卡片', { name: draggedCard.querySelector('.card-title').textContent });
}
function dragOver(event) {
if (!isAdmin || !draggedCard) return;
event.preventDefault();
const target = event.target.closest('.card');
if (target && target !== draggedCard) {
const container = target.parentElement;
const mousePositionX = event.clientX;
const targetRect = target.getBoundingClientRect();
if (mousePositionX < targetRect.left + targetRect.width / 2) {
container.insertBefore(draggedCard, target);
} else {
container.insertBefore(draggedCard, target.nextSibling);
}
}
}
function drop(event) {
if (!isAdmin || !draggedCard) return;
event.preventDefault();
draggedCard.classList.remove('dragging');
const targetCategory = event.target.closest('.card-container').id;
updateCardCategory(draggedCard, targetCategory);
logAction('放下卡片', { name: draggedCard.querySelector('.card-title').textContent, category: targetCategory });
draggedCard = null;
saveCardOrder();
}
function dragEnd(event) {
if (draggedCard) {
draggedCard.classList.remove('dragging');
logAction('拖拽卡片结束');
draggedCard = null;
}
}
// 更新卡片分类
function updateCardCategory(card, newCategory) {
const cardTitle = card.querySelector('.card-title').textContent;
const cardUrl = card.querySelector('.card-url').textContent;
const isPrivate = card.dataset.isPrivate === 'true';
const linkIndex = links.findIndex(link => link.url === cardUrl);
if (linkIndex !== -1) {
links[linkIndex].category = newCategory;
}
const linkArray = isPrivate ? privateLinks : publicLinks;
const arrayIndex = linkArray.findIndex(link => link.url === cardUrl);
if (arrayIndex !== -1) {
linkArray[arrayIndex].category = newCategory;
}
card.dataset.category = newCategory;
}
// 在页面加载完成后添加触摸事件监听器
document.addEventListener('DOMContentLoaded', function() {
const cardContainers = document.querySelectorAll('.card-container');
cardContainers.forEach(container => {
container.addEventListener('touchstart', touchStart, { passive: false });
});
});
// 保存卡片顺序
async function saveCardOrder() {
if (!isAdmin) return;
const containers = document.querySelectorAll('.card-container');
let newPublicLinks = [];
let newPrivateLinks = [];
let newCategories = {};
containers.forEach(container => {
const category = container.id;
newCategories[category] = [];
[...container.children].forEach(card => {
const url = card.querySelector('.card-url').textContent;
const name = card.querySelector('.card-title').textContent;
const isPrivate = card.dataset.isPrivate === 'true';
card.dataset.category = category;
const link = { name, url, category, isPrivate };
if (isPrivate) {
newPrivateLinks.push(link);
} else {
newPublicLinks.push(link);
}
newCategories[category].push(link);
});
});
publicLinks.length = 0;
publicLinks.push(...newPublicLinks);
privateLinks.length = 0;
privateLinks.push(...newPrivateLinks);
Object.keys(categories).forEach(key => delete categories[key]);
Object.assign(categories, newCategories);
logAction('保存卡片顺序', {
publicCount: newPublicLinks.length,
privateCount: newPrivateLinks.length,
categoryCount: Object.keys(newCategories).length
});
try {
const response = await fetch('/api/saveOrder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: 'testUser',
links: [...newPublicLinks, ...newPrivateLinks],
categories: newCategories
}),
});
const result = await response.json();
if (!result.success) {
throw new Error('Failed to save order');
}
logAction('保存卡片顺序', { publicCount: newPublicLinks.length, privateCount: newPrivateLinks.length, categoryCount: Object.keys(newCategories).length });
} catch (error) {
console.error('Error saving order:', error);
logAction('保存顺序失败', { error: error.message });
alert('保存顺序失败,请重试');
}
}
// 设置状态重新加载卡片
function reloadCardsAsAdmin() {
document.querySelectorAll('.card-container').forEach(container => {
container.innerHTML = '';
});
loadLinks().then(() => {
if (isDarkTheme) {
applyDarkTheme();
}
});
logAction('重新加载卡片(管理员模式)');
}
// 密码输入框回车事件
document.getElementById('admin-password').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
toggleSecretGarden();
}
});
// 切换设置状态
function toggleAdminMode() {
const adminBtn = document.getElementById('admin-mode-btn');
const addRemoveControls = document.querySelector('.add-remove-controls');
if (!isAdmin && isLoggedIn) {
isAdmin = true;
adminBtn.textContent = "退出设置";
addRemoveControls.style.display = 'flex';
alert('准备设置分类和书签');
reloadCardsAsAdmin();
logAction('进入设置');
} else if (isAdmin) {
isAdmin = false;
removeMode = false;
adminBtn.textContent = "设 置";
addRemoveControls.style.display = 'none';
alert('设置已保存');
reloadCardsAsAdmin();
logAction('离开设置');
}
updateUIState();
}
// 切换到登录状态
function toggleSecretGarden() {
const passwordInput = document.getElementById('admin-password');
if (!isLoggedIn) {
verifyPassword(passwordInput.value).then(isValid => {
if (isValid) {
isLoggedIn = true;
links = [...publicLinks, ...privateLinks];
loadSections();
alert('登录成功!');
logAction('登录成功');
} else {
alert('密码错误');
logAction('登录失败', { reason: '密码错误' });
}
updateUIState();
});
} else {
isLoggedIn = false;
isAdmin = false;
links = publicLinks;
loadSections();
alert('退出登录!');
updateUIState();
passwordInput.value = '';
logAction('退出登录');
}
}
// 应用暗色主题
function applyDarkTheme() {
document.body.style.backgroundColor = '#121212';
document.body.style.color = '#ffffff';
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
card.style.backgroundColor = '#1e1e1e';
card.style.color = '#ffffff';
card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)';
});
logAction('应用暗色主题');
}
// 显示添加链接对话框
function showAddDialog() {
document.getElementById('dialog-overlay').style.display = 'flex';
logAction('显示添加链接对话框');
}
// 隐藏添加链接对话框
function hideAddDialog() {
document.getElementById('dialog-overlay').style.display = 'none';
logAction('隐藏添加链接对话框');
}
// 切换删除卡片模式
function toggleRemoveMode() {
removeMode = !removeMode;
const deleteButtons = document.querySelectorAll('.delete-btn');
deleteButtons.forEach(btn => {
btn.style.display = removeMode ? 'block' : 'none';
});
logAction('切换删除卡片模式', { removeMode });
}
//切换删除分类模式
function toggleRemoveCategory() {
isRemoveCategoryMode = !isRemoveCategoryMode;
const deleteButtons = document.querySelectorAll('.delete-category-btn');
deleteButtons.forEach(btn => {
btn.style.display = isRemoveCategoryMode ? 'inline-block' : 'none';
});
logAction('切换删除分类模式', { isRemoveCategoryMode });
}
// 切换主题
function toggleTheme() {
isDarkTheme = !isDarkTheme;
document.body.style.backgroundColor = isDarkTheme ? '#121212' : '#ffffff';
document.body.style.color = isDarkTheme ? '#ffffff' : '#333';
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
card.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#a0c9e5';
card.style.color = isDarkTheme ? '#ffffff' : '#333';
card.style.boxShadow = isDarkTheme
? '0 4px 8px rgba(0, 0, 0, 0.5)'
: '0 4px 8px rgba(0, 0, 0, 0.1)';
});
const fixedElements = document.querySelectorAll('.fixed-elements');
fixedElements.forEach(element => {
element.style.backgroundColor = isDarkTheme ? '#121212' : '#ffffff';
element.style.color = isDarkTheme ? '#ffffff' : '#333';
});
const dialogBox = document.getElementById('dialog-box');
dialogBox.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#ffffff';
dialogBox.style.color = isDarkTheme ? '#ffffff' : '#333';
const inputs = document.querySelectorAll('input[type="text"], input[type="password"], select');
inputs.forEach(input => {
input.style.backgroundColor = isDarkTheme ? '#444' : '#fff';
input.style.color = isDarkTheme ? '#fff' : '#333';
input.style.borderColor = isDarkTheme ? '#555' : '#ccc';
});
logAction('切换主题', { isDarkTheme });
}
// 验证密码
async function verifyPassword(inputPassword) {
const response = await fetch('/api/verifyPassword', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: inputPassword }),
});
const result = await response.json();
return result.valid;
}
// 初始化加载链接
loadLinks();
</script>
</body>
</html>
`;
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === '/') {
return new Response(HTML_CONTENT, {
headers: { 'Content-Type': 'text/html' }
});
}
if (url.pathname === '/api/getLinks') {
const userId = url.searchParams.get('userId');
const links = await env.CARD_ORDER.get(userId);
return new Response(links || JSON.stringify([]), { status: 200 });
}
if (url.pathname === '/api/saveOrder' && request.method === 'POST') {
const { userId, links, categories } = await request.json();
await env.CARD_ORDER.put(userId, JSON.stringify({ links, categories })); //保存链接和分类
return new Response(JSON.stringify({ success: true }), { status: 200 });
}
if (url.pathname === '/api/verifyPassword' && request.method === 'POST') {
const { password } = await request.json();
const isValid = password === env.ADMIN_PASSWORD; // 从环境变量中获取密码
return new Response(JSON.stringify({ valid: isValid }), {
status: isValid ? 200 : 403,
headers: { 'Content-Type': 'application/json' },
});
}
return new Response('Not Found', { status: 404 });
}
};
2024.09.09 更新:
1)、增加私密书签,登录后可见
2)、增加网站分类管理,现在你无需编辑代码,通过页面即可进行网站分类的添加和删除操作
3)、增加搜索框和一言接口
1、原workes, 效果
const HTML_CONTENT = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Card Tab</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2280%22>⭐</text></svg>">
<style>
/* 全局样式 */
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #d8eac4;
transition: background-color 0.3s ease;
}
/* 固定元素样式 */
.fixed-elements {
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: #d8eac4;
z-index: 1000;
padding: 10px;
transition: background-color 0.3s ease;
height: 130px;
}
.fixed-elements h3 {
position: absolute;
top: 10px;
left: 20px;
margin: 0;
}
/* 中心内容样式 */
.center-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
max-width: 600px;
text-align: center;
}
/* 管理员控制面板样式 */
.admin-controls {
position: fixed;
top: 10px;
right: 10px;
font-size: 60%;
}
/* 添加/删除控制按钮样式 */
.add-remove-controls {
display: none;
flex-direction: column;
position: fixed;
right: 20px;
top: 50%;
transform: translateY(-50%);
align-items: center;
gap: 10px;
}
.round-btn {
background-color: #007bff;
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
text-align: center;
font-size: 24px;
line-height: 40px;
cursor: pointer;
margin: 5px 0;
}
.add-btn { order: 1; }
.remove-btn { order: 2; }
.category-btn { order: 3; }
.remove-category-btn { order: 4; }
/* 主要内容区域样式 */
.content {
margin-top: 140px;
padding: 20px;
}
/* 搜索栏样式 */
.search-container {
margin-top: 10px;
}
.search-bar {
display: flex;
justify-content: center;
margin-bottom: 10px;
}
.search-bar input {
width: 70%;
padding: 5px;
border: 1px solid #ccc;
border-radius: 5px 0 0 5px;
}
.search-bar button {
padding: 5px 10px;
border: 1px solid #ccc;
border-left: none;
background-color: #f8f8;
border-radius: 0 5px 5px 0;
cursor: pointer;
}
/* 搜索引擎按钮样式 */
.search-engines {
display: flex;
justify-content: center;
gap: 10px;
}
.search-engine {
padding: 5px 10px;
border: 1px solid #ccc;
background-color: #f0f0;
border-radius: 5px;
cursor: pointer;
}
/* 主题切换按钮样式 */
#theme-toggle {
position: fixed;
bottom: 50px;
right: 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
text-align: center;
font-size: 24px;
line-height: 40px;
cursor: pointer;
}
/* 显示日志按钮样式 */
#view-logs-btn {
position: fixed;
top: 100px;
right: 10px;
z-index: 1000;
}
/* 对话框样式 */
#dialog-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
}
#dialog-box {
background-color: white;
padding: 20px;
border-radius: 5px;
width: 300px;
}
#dialog-box input, #dialog-box select {
width: 100%;
margin-bottom: 10px;
padding: 5px;
}
/* 分类和卡片样式 */
.section {
margin-bottom: 20px;
}
.section-title-container {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.section-title {
font-size: 18px;
font-weight: bold;
}
.delete-category-btn {
background-color: pink;
color: white;
border: none;
padding: 5px 10px;
border-radius: 5px;
cursor: pointer;
}
.card-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.card {
background-color: #a0c9e5;
border-radius: 5px;
padding: 10px;
width: 150px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s;
position: relative;
user-select: none;
}
.card:hover {
transform: translateY(-5px);
}
.card-top {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.card-icon {
width: 16px;
height: 16px;
margin-right: 5px;
}
.card-title {
font-size: 14px;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-url {
font-size: 12px;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.private-tag {
background-color: #ff9800;
color: white;
font-size: 10px;
padding: 2px 5px;
border-radius: 3px;
position: absolute;
top: 5px;
right: 5px;
}
.delete-btn {
position: absolute;
top: -10px;
right: -10px;
background-color: red;
color: white;
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
text-align: center;
font-size: 14px;
line-height: 20px;
cursor: pointer;
display: none;
}
/* 版权信息样式 */
#copyright {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 40px;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
z-index: 1000;
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
}
#copyright p {
margin: 0;
}
#copyright a {
color: #007bff;
text-decoration: none;
}
#copyright a:hover {
text-decoration: underline;
}
/* 响应式设计 */
@media (max-width: 480px) {
.fixed-elements {
position: relative;
padding: 5px;
}
.content {
margin-top: 10px;
}
.admin-controls input,
.admin-controls button {
height: 30%;
}
.card-container {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.card {
width: 80%;
max-width: 100%;
padding: 5px;
}
.card-title {
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 130px;
}
.card-url {
font-size: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 130px;
}
.add-remove-controls {
right: 2px;
}
.round-btn,
#theme-toggle {
right: 5px;
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
font-size: 24px;
}
}
</style>
</head>
<body>
<div class="fixed-elements">
<h3>我的导航</h3>
<div class="center-content">
<!-- 一言模块 -->
<p id="hitokoto">
<a href="#" id="hitokoto_text"></a>
</p>
<script src="https://v1.hitokoto.cn/?encode=js&select=%23hitokoto" defer></script>
<!-- 搜索栏 -->
<div class="search-container">
<div class="search-bar">
<input type="text" id="search-input" placeholder="">
<button id="search-button">🔍</button>
</div>
<div class="search-engines">
<button class="search-engine" data-engine="baidu">百度</button>
<button class="search-engine" data-engine="bing">必应</button>
<button class="search-engine" data-engine="google">谷歌</button>
</div>
</div>
</div>
<!-- 管理员控制面板 -->
<div class="admin-controls">
<input type="password" id="admin-password" placeholder="输入密码">
<button id="admin-mode-btn" onclick="toggleAdminMode()">设 置</button>
<button id="secret-garden-btn" onclick="toggleSecretGarden()">登 录</button>
</div>
</div>
<div class="content">
<!-- 添加/删除控制按钮 -->
<div class="add-remove-controls">
<button class="round-btn add-btn" onclick="showAddDialog()">+</button>
<button class="round-btn remove-btn" onclick="toggleRemoveMode()">-</button>
<button class="round-btn category-btn" onclick="addCategory()">C+</button>
<button class="round-btn remove-category-btn" onclick="toggleRemoveCategory()">C-</button>
</div>
<!-- 分类和卡片容器 -->
<div id="sections-container"></div>
<!-- 主题切换按钮 -->
<button id="theme-toggle" onclick="toggleTheme()">◑</button>
<!-- 显示日志按钮 用于调试-->
<!--<button id="view-logs-btn" onclick="viewLogs()">显示日志</button>-->
<!-- 添加链接对话框 -->
<div id="dialog-overlay">
<div id="dialog-box">
<label for="name-input">名称</label>
<input type="text" id="name-input">
<label for="url-input">地址</label>
<input type="text" id="url-input">
<label for="category-select">选择分类</label>
<select id="category-select"></select>
<div class="private-link-container">
<label for="private-checkbox">私密链接</label>
<input type="checkbox" id="private-checkbox">
</div>
<button onclick="addLink()">确定</button>
<button onclick="hideAddDialog()">取消</button>
</div>
</div>
<!-- 版权信息 -->
<div id="copyright" class="copyright">
<!--请不要删除-->
<p>项目地址:<a href="https://github.com/hmhm2022/Card-Tab" target="_blank">GitHub</a> 如果喜欢,烦请点个star!</p>
</div>
</div>
<script>
// 搜索引擎配置
const searchEngines = {
baidu: "https://www.baidu.com/s?wd=",
bing: "https://www.bing.com/search?q=",
google: "https://www.google.com/search?q="
};
let currentEngine = "baidu";
// 日志记录函数
function logAction(action, details) {
const timestamp = new Date().toISOString();
const logEntry = timestamp + ': ' + action + ' - ' + JSON.stringify(details);
let logs = JSON.parse(localStorage.getItem('cardTabLogs') || '[]');
logs.push(logEntry);
// 保留最新的1000条日志
if (logs.length > 1000) {
logs = logs.slice(-1000);
}
localStorage.setItem('cardTabLogs', JSON.stringify(logs));
console.log(logEntry); // 同时在控制台输出日志
}
// 查看日志的函数
function viewLogs() {
const logs = JSON.parse(localStorage.getItem('cardTabLogs') || '[]');
console.log('Card Tab Logs:');
logs.forEach(log => console.log(log));
alert('日志已在控制台输出,请按F12打开开发者工具查看。');
}
// 设置当前搜索引擎
function setActiveEngine(engine) {
currentEngine = engine;
document.querySelectorAll('.search-engine').forEach(btn => {
btn.style.backgroundColor = btn.dataset.engine === engine ? '#c0c0c0' : '#f0f0f0';
});
logAction('设置搜索引擎', { engine });
}
// 搜索引擎按钮点击事件
document.querySelectorAll('.search-engine').forEach(button => {
button.addEventListener('click', () => setActiveEngine(button.dataset.engine));
});
// 搜索按钮点击事件
document.getElementById('search-button').addEventListener('click', () => {
const query = document.getElementById('search-input').value;
if (query) {
logAction('执行搜索', { engine: currentEngine, query });
window.open(searchEngines[currentEngine] + encodeURIComponent(query), '_blank');
}
});
// 搜索输入框回车事件
document.getElementById('search-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
document.getElementById('search-button').click();
}
});
// 初始化搜索引擎
setActiveEngine(currentEngine);
// 全局变量
let publicLinks = [];
let privateLinks = [];
let isAdmin = false;
let isLoggedIn = false;
let removeMode = false;
let isRemoveCategoryMode = false;
let isDarkTheme = false;
let links = [];
const categories = {};
// 添加新分类
function addCategory() {
const categoryName = prompt('请输入新分类名称:');
if (categoryName && !categories[categoryName]) {
categories[categoryName] = [];
updateCategorySelect();
renderCategories();
saveLinks();
logAction('添加分类', { categoryName, currentLinkCount: links.length });
} else if (categories[categoryName]) {
alert('该分类已存在');
logAction('添加分类失败', { categoryName, reason: '分类已存在' });
}
}
// 删除分类
function deleteCategory(category) {
if (confirm('确定要删除 "' + category + '" 分类吗?这将删除该分类下的所有链接。')) {
delete categories[category];
links = links.filter(link => link.category !== category);
publicLinks = publicLinks.filter(link => link.category !== category);
privateLinks = privateLinks.filter(link => link.category !== category);
updateCategorySelect();
saveLinks();
renderCategories();
logAction('删除分类', { category });
}
}
// 渲染分类(不重新加载链接)
function renderCategories() {
const container = document.getElementById('sections-container');
container.innerHTML = '';
Object.keys(categories).forEach(category => {
const section = document.createElement('div');
section.className = 'section';
const titleContainer = document.createElement('div');
titleContainer.className = 'section-title-container';
const title = document.createElement('div');
title.className = 'section-title';
title.textContent = category;
titleContainer.appendChild(title);
if (isAdmin) {
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '删除分类';
deleteBtn.className = 'delete-category-btn';
deleteBtn.style.display = isRemoveCategoryMode ? 'inline-block' : 'none';
deleteBtn.onclick = () => deleteCategory(category);
titleContainer.appendChild(deleteBtn);
}
const cardContainer = document.createElement('div');
cardContainer.className = 'card-container';
cardContainer.id = category;
section.appendChild(titleContainer);
section.appendChild(cardContainer);
container.appendChild(section);
const categoryLinks = links.filter(link => link.category === category);
categoryLinks.forEach(link => {
createCard(link, cardContainer);
});
});
logAction('渲染分类', { categoryCount: Object.keys(categories).length, linkCount: links.length });
}
// 读取链接数据
async function loadLinks() {
const response = await fetch('/api/getLinks?userId=testUser');
const data = await response.json();
if (data.categories) {
Object.assign(categories, data.categories);
}
publicLinks = data.links ? data.links.filter(link => !link.isPrivate) : [];
privateLinks = data.links ? data.links.filter(link => link.isPrivate) : [];
links = isLoggedIn ? [...publicLinks, ...privateLinks] : publicLinks;
loadSections();
updateCategorySelect();
updateUIState();
logAction('读取链接', { publicCount: publicLinks.length, privateCount: privateLinks.length });
}
// 更新UI状态
function updateUIState() {
const passwordInput = document.getElementById('admin-password');
const adminBtn = document.getElementById('admin-mode-btn');
const secretGardenBtn = document.getElementById('secret-garden-btn');
const addRemoveControls = document.querySelector('.add-remove-controls');
passwordInput.style.display = isLoggedIn ? 'none' : 'inline-block';
secretGardenBtn.textContent = isLoggedIn ? "退出" : "登录";
secretGardenBtn.style.display = 'inline-block';
if (isAdmin) {
adminBtn.textContent = "离开设置";
adminBtn.style.display = 'inline-block';
addRemoveControls.style.display = 'flex';
} else if (isLoggedIn) {
adminBtn.textContent = "设置";
adminBtn.style.display = 'inline-block';
addRemoveControls.style.display = 'none';
} else {
adminBtn.style.display = 'none';
addRemoveControls.style.display = 'none';
}
logAction('更新UI状态', { isAdmin, isLoggedIn });
}
// 登录状态显示(加载所有链接)
function showSecretGarden() {
if (isLoggedIn) {
links = [...publicLinks, ...privateLinks];
loadSections();
// 显示所有私密标签
document.querySelectorAll('.private-tag').forEach(tag => {
tag.style.display = 'block';
});
logAction('显示私密花园');
}
}
// 加载分类和链接
function loadSections() {
const container = document.getElementById('sections-container');
container.innerHTML = '';
Object.keys(categories).forEach(category => {
const section = document.createElement('div');
section.className = 'section';
const titleContainer = document.createElement('div');
titleContainer.className = 'section-title-container';
const title = document.createElement('div');
title.className = 'section-title';
title.textContent = category;
titleContainer.appendChild(title);
if (isAdmin) {
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '删除分类';
deleteBtn.className = 'delete-category-btn';
deleteBtn.style.display = 'none';
deleteBtn.onclick = () => deleteCategory(category);
titleContainer.appendChild(deleteBtn);
}
const cardContainer = document.createElement('div');
cardContainer.className = 'card-container';
cardContainer.id = category;
section.appendChild(titleContainer);
section.appendChild(cardContainer);
let privateCount = 0;
let linkCount = 0;
links.forEach(link => {
if (link.category === category) {
if (link.isPrivate) privateCount++;
linkCount++;
createCard(link, cardContainer);
}
});
if (privateCount < linkCount || isLoggedIn) {
container.appendChild(section);
}
});
logAction('加载分类和链接', { isAdmin: isAdmin, linkCount: links.length, categoryCount: Object.keys(categories).length });
}
// 创建卡片
function createCard(link, container) {
const card = document.createElement('div');
card.className = 'card';
card.setAttribute('draggable', isAdmin);
card.dataset.isPrivate = link.isPrivate;
const cardTop = document.createElement('div');
cardTop.className = 'card-top';
const icon = document.createElement('img');
icon.className = 'card-icon';
icon.src = 'https://favicon.zhusl.com/ico?url=' + link.url;
icon.alt = 'Website Icon';
const title = document.createElement('div');
title.className = 'card-title';
title.textContent = link.name;
cardTop.appendChild(icon);
cardTop.appendChild(title);
const url = document.createElement('div');
url.className = 'card-url';
url.textContent = link.url;
card.appendChild(cardTop);
card.appendChild(url);
if (link.isPrivate) {
const privateTag = document.createElement('div');
privateTag.className = 'private-tag';
privateTag.textContent = '私密';
card.appendChild(privateTag);
}
const correctedUrl = link.url.startsWith('http://') || link.url.startsWith('https://') ? link.url : 'http://' + link.url;
if (!isAdmin) {
card.addEventListener('click', () => {
window.open(correctedUrl, '_blank');
logAction('打开链接', { name: link.name, url: correctedUrl });
});
}
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '–';
deleteBtn.className = 'delete-btn';
deleteBtn.onclick = function (event) {
event.stopPropagation();
removeCard(card);
};
card.appendChild(deleteBtn);
updateCardStyle(card);
card.addEventListener('dragstart', dragStart);
card.addEventListener('dragover', dragOver);
card.addEventListener('dragend', dragEnd);
card.addEventListener('drop', drop);
card.addEventListener('touchstart', touchStart, { passive: false });
if (isAdmin && removeMode) {
deleteBtn.style.display = 'block';
}
if (isAdmin || (link.isPrivate && isLoggedIn) || !link.isPrivate) {
container.appendChild(card);
}
// logAction('创建卡片', { name: link.name, isPrivate: link.isPrivate });
}
// 更新卡片样式
function updateCardStyle(card) {
if (isDarkTheme) {
card.style.backgroundColor = '#1e1e1e';
card.style.color = '#ffffff';
card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)';
} else {
card.style.backgroundColor = '#a0c9e5';
card.style.color = '#333';
card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)';
}
}
// 更新分类选择下拉框
function updateCategorySelect() {
const categorySelect = document.getElementById('category-select');
categorySelect.innerHTML = '';
Object.keys(categories).forEach(category => {
const option = document.createElement('option');
option.value = category;
option.textContent = category;
categorySelect.appendChild(option);
});
logAction('更新分类选择', { categoryCount: Object.keys(categories).length });
}
// 保存链接数据
async function saveLinks() {
let allLinks = [...publicLinks, ...privateLinks];
try {
await fetch('/api/saveOrder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: 'testUser',
links: allLinks,
categories: categories
}),
});
logAction('保存链接', { linkCount: allLinks.length, categoryCount: Object.keys(categories).length });
} catch (error) {
console.error('Error saving links:', error);
logAction('保存链接失败', { error: error.message });
alert('保存链接失败,请重试');
}
}
// 添加卡片弹窗
function addLink() {
const name = document.getElementById('name-input').value;
const url = document.getElementById('url-input').value;
const category = document.getElementById('category-select').value;
const isPrivate = document.getElementById('private-checkbox').checked;
if (name && url && category) {
const newLink = { name, url, category, isPrivate };
if (isPrivate) {
privateLinks.push(newLink);
} else {
publicLinks.push(newLink);
}
links = isLoggedIn ? [...publicLinks, ...privateLinks] : publicLinks;
if (isAdmin || (isPrivate && isLoggedIn) || !isPrivate) {
const container = document.getElementById(category);
if (container) {
createCard(newLink, container);
} else {
categories[category] = [];
renderCategories();
}
}
saveLinks();
document.getElementById('name-input').value = '';
document.getElementById('url-input').value = '';
document.getElementById('private-checkbox').checked = false;
hideAddDialog();
logAction('添加卡片', { name, url, category, isPrivate });
}
}
// 删除卡片
function removeCard(card) {
const name = card.querySelector('.card-title').textContent;
const url = card.querySelector('.card-url').textContent;
const isPrivate = card.dataset.isPrivate === 'true';
links = links.filter(link => link.url !== url);
if (isPrivate) {
privateLinks = privateLinks.filter(link => link.url !== url);
} else {
publicLinks = publicLinks.filter(link => link.url !== url);
}
for (const key in categories) {
categories[key] = categories[key].filter(link => link.url !== url);
}
card.remove();
saveLinks();
logAction('删除卡片', { name, url, isPrivate });
}
// 拖拽卡片
let draggedCard = null;
let touchStartX, touchStartY;
// 触屏端拖拽卡片
function touchStart(event) {
if (!isAdmin) return;
draggedCard = event.target.closest('.card');
if (!draggedCard) return;
event.preventDefault();
const touch = event.touches[0];
touchStartX = touch.clientX;
touchStartY = touch.clientY;
draggedCard.classList.add('dragging');
document.addEventListener('touchmove', touchMove, { passive: false });
document.addEventListener('touchend', touchEnd);
}
function touchMove(event) {
if (!draggedCard) return;
event.preventDefault();
const touch = event.touches[0];
const currentX = touch.clientX;
const currentY = touch.clientY;
const deltaX = currentX - touchStartX;
const deltaY = currentY - touchStartY;
draggedCard.style.transform = "translate(" + deltaX + "px, " + deltaY + "px)";
const target = findCardUnderTouch(currentX, currentY);
if (target && target !== draggedCard) {
const container = target.parentElement;
const targetRect = target.getBoundingClientRect();
if (currentX < targetRect.left + targetRect.width / 2) {
container.insertBefore(draggedCard, target);
} else {
container.insertBefore(draggedCard, target.nextSibling);
}
}
}
function touchEnd(event) {
if (!draggedCard) return;
document.removeEventListener('touchmove', touchMove);
document.removeEventListener('touchend', touchEnd);
draggedCard.classList.remove('dragging');
draggedCard.style.transform = '';
const targetCategory = draggedCard.closest('.card-container').id;
updateCardCategory(draggedCard, targetCategory);
saveCardOrder();
draggedCard = null;
}
function findCardUnderTouch(x, y) {
const cards = document.querySelectorAll('.card:not(.dragging)');
return Array.from(cards).find(card => {
const rect = card.getBoundingClientRect();
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
});
}
// PC端拖拽卡片
function dragStart(event) {
if (!isAdmin) return;
draggedCard = event.target.closest('.card');
if (!draggedCard) return;
draggedCard.classList.add('dragging');
event.dataTransfer.effectAllowed = "move";
logAction('开始拖拽卡片', { name: draggedCard.querySelector('.card-title').textContent });
}
function dragOver(event) {
if (!isAdmin || !draggedCard) return;
event.preventDefault();
const target = event.target.closest('.card');
if (target && target !== draggedCard) {
const container = target.parentElement;
const mousePositionX = event.clientX;
const targetRect = target.getBoundingClientRect();
if (mousePositionX < targetRect.left + targetRect.width / 2) {
container.insertBefore(draggedCard, target);
} else {
container.insertBefore(draggedCard, target.nextSibling);
}
}
}
function drop(event) {
if (!isAdmin || !draggedCard) return;
event.preventDefault();
draggedCard.classList.remove('dragging');
const targetCategory = event.target.closest('.card-container').id;
updateCardCategory(draggedCard, targetCategory);
logAction('放下卡片', { name: draggedCard.querySelector('.card-title').textContent, category: targetCategory });
draggedCard = null;
saveCardOrder();
}
function dragEnd(event) {
if (draggedCard) {
draggedCard.classList.remove('dragging');
logAction('拖拽卡片结束');
draggedCard = null;
}
}
// 更新卡片分类
function updateCardCategory(card, newCategory) {
const cardTitle = card.querySelector('.card-title').textContent;
const cardUrl = card.querySelector('.card-url').textContent;
const isPrivate = card.dataset.isPrivate === 'true';
const linkIndex = links.findIndex(link => link.url === cardUrl);
if (linkIndex !== -1) {
links[linkIndex].category = newCategory;
}
const linkArray = isPrivate ? privateLinks : publicLinks;
const arrayIndex = linkArray.findIndex(link => link.url === cardUrl);
if (arrayIndex !== -1) {
linkArray[arrayIndex].category = newCategory;
}
card.dataset.category = newCategory;
}
// 在页面加载完成后添加触摸事件监听器
document.addEventListener('DOMContentLoaded', function() {
const cardContainers = document.querySelectorAll('.card-container');
cardContainers.forEach(container => {
container.addEventListener('touchstart', touchStart, { passive: false });
});
});
// 保存卡片顺序
async function saveCardOrder() {
if (!isAdmin) return;
const containers = document.querySelectorAll('.card-container');
let newPublicLinks = [];
let newPrivateLinks = [];
let newCategories = {};
containers.forEach(container => {
const category = container.id;
newCategories[category] = [];
[...container.children].forEach(card => {
const url = card.querySelector('.card-url').textContent;
const name = card.querySelector('.card-title').textContent;
const isPrivate = card.dataset.isPrivate === 'true';
card.dataset.category = category;
const link = { name, url, category, isPrivate };
if (isPrivate) {
newPrivateLinks.push(link);
} else {
newPublicLinks.push(link);
}
newCategories[category].push(link);
});
});
publicLinks.length = 0;
publicLinks.push(...newPublicLinks);
privateLinks.length = 0;
privateLinks.push(...newPrivateLinks);
Object.keys(categories).forEach(key => delete categories[key]);
Object.assign(categories, newCategories);
logAction('保存卡片顺序', {
publicCount: newPublicLinks.length,
privateCount: newPrivateLinks.length,
categoryCount: Object.keys(newCategories).length
});
try {
const response = await fetch('/api/saveOrder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: 'testUser',
links: [...newPublicLinks, ...newPrivateLinks],
categories: newCategories
}),
});
const result = await response.json();
if (!result.success) {
throw new Error('Failed to save order');
}
logAction('保存卡片顺序', { publicCount: newPublicLinks.length, privateCount: newPrivateLinks.length, categoryCount: Object.keys(newCategories).length });
} catch (error) {
console.error('Error saving order:', error);
logAction('保存顺序失败', { error: error.message });
alert('保存顺序失败,请重试');
}
}
// 设置状态重新加载卡片
function reloadCardsAsAdmin() {
document.querySelectorAll('.card-container').forEach(container => {
container.innerHTML = '';
});
loadLinks().then(() => {
if (isDarkTheme) {
applyDarkTheme();
}
});
logAction('重新加载卡片(管理员模式)');
}
// 密码输入框回车事件
document.getElementById('admin-password').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
toggleSecretGarden();
}
});
// 切换设置状态
function toggleAdminMode() {
const adminBtn = document.getElementById('admin-mode-btn');
const addRemoveControls = document.querySelector('.add-remove-controls');
if (!isAdmin && isLoggedIn) {
isAdmin = true;
adminBtn.textContent = "退出设置";
addRemoveControls.style.display = 'flex';
alert('准备设置分类和书签');
reloadCardsAsAdmin();
logAction('进入设置');
} else if (isAdmin) {
isAdmin = false;
removeMode = false;
adminBtn.textContent = "设 置";
addRemoveControls.style.display = 'none';
alert('设置已保存');
reloadCardsAsAdmin();
logAction('离开设置');
}
updateUIState();
}
// 切换到登录状态
function toggleSecretGarden() {
const passwordInput = document.getElementById('admin-password');
if (!isLoggedIn) {
verifyPassword(passwordInput.value).then(isValid => {
if (isValid) {
isLoggedIn = true;
links = [...publicLinks, ...privateLinks];
loadSections();
alert('登录成功!');
logAction('登录成功');
} else {
alert('密码错误');
logAction('登录失败', { reason: '密码错误' });
}
updateUIState();
});
} else {
isLoggedIn = false;
isAdmin = false;
links = publicLinks;
loadSections();
alert('退出登录!');
updateUIState();
passwordInput.value = '';
logAction('退出登录');
}
}
// 应用暗色主题
function applyDarkTheme() {
document.body.style.backgroundColor = '#121212';
document.body.style.color = '#ffffff';
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
card.style.backgroundColor = '#1e1e1e';
card.style.color = '#ffffff';
card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)';
});
logAction('应用暗色主题');
}
// 显示添加链接对话框
function showAddDialog() {
document.getElementById('dialog-overlay').style.display = 'flex';
logAction('显示添加链接对话框');
}
// 隐藏添加链接对话框
function hideAddDialog() {
document.getElementById('dialog-overlay').style.display = 'none';
logAction('隐藏添加链接对话框');
}
// 切换删除卡片模式
function toggleRemoveMode() {
removeMode = !removeMode;
const deleteButtons = document.querySelectorAll('.delete-btn');
deleteButtons.forEach(btn => {
btn.style.display = removeMode ? 'block' : 'none';
});
logAction('切换删除卡片模式', { removeMode });
}
//切换删除分类模式
function toggleRemoveCategory() {
isRemoveCategoryMode = !isRemoveCategoryMode;
const deleteButtons = document.querySelectorAll('.delete-category-btn');
deleteButtons.forEach(btn => {
btn.style.display = isRemoveCategoryMode ? 'inline-block' : 'none';
});
logAction('切换删除分类模式', { isRemoveCategoryMode });
}
// 切换主题
function toggleTheme() {
isDarkTheme = !isDarkTheme;
document.body.style.backgroundColor = isDarkTheme ? '#121212' : '#d8eac4';
document.body.style.color = isDarkTheme ? '#ffffff' : '#333';
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
card.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#a0c9e5';
card.style.color = isDarkTheme ? '#ffffff' : '#333';
card.style.boxShadow = isDarkTheme
? '0 4px 8px rgba(0, 0, 0, 0.5)'
: '0 4px 8px rgba(0, 0, 0, 0.1)';
});
const fixedElements = document.querySelectorAll('.fixed-elements');
fixedElements.forEach(element => {
element.style.backgroundColor = isDarkTheme ? '#121212' : '#d8eac4';
element.style.color = isDarkTheme ? '#ffffff' : '#333';
});
const dialogBox = document.getElementById('dialog-box');
dialogBox.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#ffffff';
dialogBox.style.color = isDarkTheme ? '#ffffff' : '#333';
const inputs = document.querySelectorAll('input[type="text"], input[type="password"], select');
inputs.forEach(input => {
input.style.backgroundColor = isDarkTheme ? '#444' : '#fff';
input.style.color = isDarkTheme ? '#fff' : '#333';
input.style.borderColor = isDarkTheme ? '#555' : '#ccc';
});
logAction('切换主题', { isDarkTheme });
}
// 验证密码
async function verifyPassword(inputPassword) {
const response = await fetch('/api/verifyPassword', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: inputPassword }),
});
const result = await response.json();
return result.valid;
}
// 初始化加载链接
loadLinks();
</script>
</body>
</html>
`;
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === '/') {
return new Response(HTML_CONTENT, {
headers: { 'Content-Type': 'text/html' }
});
}
if (url.pathname === '/api/getLinks') {
const userId = url.searchParams.get('userId');
const links = await env.CARD_ORDER.get(userId);
return new Response(links || JSON.stringify([]), { status: 200 });
}
if (url.pathname === '/api/saveOrder' && request.method === 'POST') {
const { userId, links, categories } = await request.json();
await env.CARD_ORDER.put(userId, JSON.stringify({ links, categories })); //保存链接和分类
return new Response(JSON.stringify({ success: true }), { status: 200 });
}
if (url.pathname === '/api/verifyPassword' && request.method === 'POST') {
const { password } = await request.json();
const isValid = password === env.ADMIN_PASSWORD; // 从环境变量中获取密码
return new Response(JSON.stringify({ valid: isValid }), {
status: isValid ? 200 : 403,
headers: { 'Content-Type': 'application/json' },
});
}
return new Response('Not Found', { status: 404 });
}
};
修改后workes, 效果
const HTML_CONTENT = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ws01主页</title>
<link rel="icon" href="https://cdn.glitch.global/efdace30-a873-49c7-aaa9-4fa31679ee0c/thumbnails%2F%E5%9B%BE%E6%A0%8701.jpg?1692046715299">
<style>
/* 全局样式 */
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #d4d4d4;
transition: background-color 0.3s ease;
}
ul {
padding: 0;
margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 0px;
margin-inline-end: 0px;
padding-inline-start: 40px;
unicode-bidi: isolate;
}
li {
display: list-item;
text-align: -webkit-match-parent;
unicode-bidi: isolate;
margin: 0 8px;
}
.background {
background-image: linear-gradient(#d4d4d4 1px, transparent 0), linear-gradient(90deg, #d4d4d4 1px, transparent 0);
background-size: 32px 32px;
background-color: #fffcf8;
}
.header {
background-color: #fff;
box-shadow: 0 0 5px rgba(0, 0, 0, .1);
transition: background-color .5s;
}
.navbar {
display: flex;
width: 1280px;
margin: auto;
height: 40px;
}
.navbar .brand {
display: flex;
align-items: center;
color: #555;
}
.brand .logo {
max-width: 36px;
}
.brand .title {
margin-left: 5px;
font-family: helvetica neue, helvetica, arial, sans-serif;
font-size: 24px;
font-weight: 700;
}
.beta {
color: #ccc;
font-size: 11px;
font-weight: 400;
position: relative;
top: -14px;
}
.category-list {
list-style: none;
display: flex;
align-items: center;
}
.sites {
width:1286px;
background:;
border:2px solid auto;
margin:15px auto;
padding:0px;
text-align:center;
}
.sites1 {
width:1286px;
background:;
border:2px solid auto;
margin:15px auto;
padding:0px;
}
.sites dl {
height:36px;
line-height:36px;
display:block;
margin:0;
}
.sites dl.alt {
background:#d4d4d4;
border-top:1px solid #ffffff;
border-bottom:1px solid #ffffff;
}
.sites dl.alt2 {
background:#d4d4d4;
border-top:1px solid #ffffff;
border-bottom:1px solid #ffffff;
}
.sites dt,.sites dd {
text-align:center;
display:block;
float:left;
}
.sites dt {
width:60px;
}
.sites dd {
width:90px;
margin:0;
}
.footer {
width:580px;
text-align:center;
margin:5px auto;
padding:2px;
}
/* 固定元素样式 */
.fixed-elements {
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: #d4d4d4;
z-index: 1000;
padding: 10px;
transition: background-color 0.3s ease;
height: 130px;
}
.fixed-elements h3 {
position: absolute;
top: 10px;
left: 20px;
margin: 0;
}
/* 中心内容样式 */
.center-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
max-width: 600px;
text-align: center;
}
/* 管理员控制面板样式 */
.admin-controls {
position: fixed;
top: 10px;
right: 10px;
font-size: 60%;
}
/* 添加/删除控制按钮样式 */
.add-remove-controls {
display: none;
flex-direction: column;
position: fixed;
right: 20px;
top: 50%;
transform: translateY(-50%);
align-items: center;
gap: 10px;
}
.round-btn {
background-color: #007bff;
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
text-align: center;
font-size: 24px;
line-height: 40px;
cursor: pointer;
margin: 5px 0;
}
.add-btn { order: 1; }
.remove-btn { order: 2; }
.category-btn { order: 3; }
.remove-category-btn { order: 4; }
/* 主要内容区域样式 */
.content {
margin-top: 140px;
padding: 20px;
}
/* 搜索栏样式 */
.search-container {
margin-top: 10px;
}
.search-bar {
display: flex;
justify-content: center;
margin-bottom: 10px;
}
.search-bar input {
width: 50%;
padding: 5px;
border: 1px solid #ccc;
border-radius: 5px 0 0 5px;
}
.search-bar button {
padding: 5px 20px;
border: 1px solid #ccc;
border-left: none;
background-color: #a0c9e5;
border-radius: 0 5px 5px 0;
cursor: pointer;
}
/* 搜索引擎按钮样式 */
.search-engines {
display: flex;
justify-content: center;
gap: 10px;
}
.search-engine {
padding: 5px 10px;
border: 1px solid #ccc;
background-color: #f0f0;
border-radius: 5px;
cursor: pointer;
}
/* 主题切换按钮样式 */
#theme-toggle {
position: fixed;
bottom: 50px;
right: 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
text-align: center;
font-size: 24px;
line-height: 40px;
cursor: pointer;
}
/* 显示日志按钮样式 */
#view-logs-btn {
position: fixed;
top: 100px;
right: 10px;
z-index: 1000;
}
/* 对话框样式 */
#dialog-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
}
#dialog-box {
background-color: white;
padding: 20px;
border-radius: 5px;
width: 300px;
}
#dialog-box input, #dialog-box select {
width: 100%;
margin-bottom: 10px;
padding: 5px;
}
/* 分类和卡片样式 */
.section {
margin-bottom: 20px;
}
.section-title-container {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.section-title {
font-size: 18px;
font-weight: bold;
margin-left: 20px; /* 分类标签右移添加这一行 */
color: green; /* 分类标签字体颜色添加这一行 */
}
.delete-category-btn {
background-color: pink;
color: white;
border: none;
padding: 5px 10px;
border-radius: 5px;
cursor: pointer;
}
.card-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.card {
background-color: #a0c9e5;
border-radius: 5px;
padding: 10px;
width: 132px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s;
position: relative;
}
/* 中等屏幕 (平板等) */
@media (max-width: 1024px) {
}
/* 小屏幕 (手机) */
@media (max-width: 768px) {
}
/* 超小屏幕 (更小手机) */
@media (max-width: 480px) {
.card {
padding: 6px; /* 进一步减小卡片内边距6 */
width: 235px;
}
}
.card:hover {
transform: translateY(-5px);
}
.card-top {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.card-icon {
width: 20px;
height: 20px;
margin-right: 5px;
}
.card-title {
font-size: 18px;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-url {
font-size: 12px;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.private-tag {
background-color: #ff9800;
color: white;
font-size: 10px;
padding: 2px 5px;
border-radius: 3px;
position: absolute;
top: 5px;
right: 5px;
}
.delete-btn {
position: absolute;
top: -10px;
right: -10px;
background-color: red;
color: white;
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
text-align: center;
font-size: 14px;
line-height: 20px;
cursor: pointer;
display: none;
}
/* 版权信息样式 */
#copyright {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 40px;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
z-index: 1000;
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
}
#copyright p {
margin: 0;
}
#copyright a {
color: #007bff;
text-decoration: none;
}
#copyright a:hover {
text-decoration: underline;
}
</style>
</head>
<body class="background">
</div>
<div class="sites"> <!-- 00 -->
<header class="header">
<nav class="navbar">
<a href="https://www.199881.xyz/" class="brand">
<img class="debug logo" src="https://cdn.glitch.global/efdace30-a873-49c7-aaa9-4fa31679ee0c/thumbnails%2F%E5%9B%BE%E6%A0%8701.jpg?1692046715299">
<span class="debug title">WS01の主页</span>
<span class="debug beta">beta</span>
<!-- 一言模块 -->
<p id="hitokoto">
<a href="#" id="hitokoto_text"></a>
</p>
<script src="https://v1.hitokoto.cn/?encode=js&select=%23hitokoto" defer></script>
</a>
</header>
<!-- 搜索栏 -->
<div class="search-container">
<div class="search-bar">
<input type="text" id="search-input" placeholder="">
<button id="search-button">🔍</button>
</div>
<div class="search-engines">
<button class="search-engine" data-engine="baidu">百度</button>
<button class="search-engine" data-engine="bing">必应</button>
<button class="search-engine" data-engine="toutiao">头条</button>
<button class="search-engine" data-engine="sm">神马</button>
<button class="search-engine" data-engine="bilibili">哔哩</button>
<button class="search-engine" data-engine="github">github</button>
<button class="search-engine" data-engine="google">谷歌</button>
<button class="search-engine" data-engine="yandex">yandex</button>
</div>
</div>
<div class="head">
<!-- 添加/删除控制按钮 -->
<div class="add-remove-controls">
<button class="round-btn add-btn" onclick="showAddDialog()">+</button>
<button class="round-btn remove-btn" onclick="toggleRemoveMode()">-</button>
<button class="round-btn category-btn" onclick="addCategory()">C+</button>
<button class="round-btn remove-category-btn" onclick="toggleRemoveCategory()">C-</button>
</div>
<div class="sites1"> <!-- 00 -->
<!-- 分类和卡片容器 -->
<div id="sections-container"></div>
<!-- 主题切换按钮 -->
<button id="theme-toggle" onclick="toggleTheme()">◑</button>
<!-- 显示日志按钮 用于调试-->
<!--<button id="view-logs-btn" onclick="viewLogs()">显示日志</button>-->
<!-- 添加链接对话框 -->
<div id="dialog-overlay">
<div id="dialog-box">
<label for="name-input">名称</label>
<input type="text" id="name-input">
<label for="url-input">地址</label>
<input type="text" id="url-input">
<label for="category-select">选择分类</label>
<select id="category-select"></select>
<div class="private-link-container">
<label for="private-checkbox">私密链接</label>
<input type="checkbox" id="private-checkbox">
</div>
<button onclick="addLink()">确定</button>
<button onclick="hideAddDialog()">取消</button>
</div>
</div>
<br />
<!-- body 页脚 -->
<div class="footer">
<!-- 管理员控制面板 -->
<input type="password" id="admin-password" placeholder="输入密码">
<button id="admin-mode-btn" onclick="toggleAdminMode()">设 置</button>
<button id="secret-garden-btn" onclick="toggleSecretGarden()">登 录</button>
<br />
<br /> <br />
<!-- 版权信息 -->
<div id="copyright" class="copyright">
<!--请不要删除-->
<p><a href="https://github.com/hmhm2022/Card-Tab" target="_blank">GitHub</a> 项目
<!-- 开站时间开始 -->
<span id="timeDate">载入天数...</span><span id="times">载入时分秒...</span> <script language="javascript">
var now = new Date();
function createtime(){
var grt= new Date("09/05/2024 00:00:00");/*---这里是网站的启用时间--*/
now.setTime(now.getTime()+250);
days = (now - grt ) / 1000 / 60 / 60 / 24;
dnum = Math.floor(days);
hours = (now - grt ) / 1000 / 60 / 60 - (24 * dnum);
hnum = Math.floor(hours);
if(String(hnum).length ==1 ){hnum = "0" + hnum;}
minutes = (now - grt ) / 1000 /60 - (24 * 60 * dnum) - (60 * hnum);
mnum = Math.floor(minutes);
if(String(mnum).length ==1 ){mnum = "0" + mnum;}
seconds = (now - grt ) / 1000 - (24 * 60 * 60 * dnum) - (60 * 60 * hnum) - (60 * mnum);
snum = Math.round(seconds);
if(String(snum).length ==1 ){snum = "0" + snum;}
document.getElementById("timeDate").innerHTML = "稳定运行"+dnum+"天";
document.getElementById("times").innerHTML = hnum + "小时" + mnum + "分" + snum + "秒";
}
setInterval("createtime()",250);
</script>
<!-- 开站时间结束 -->
</p>
</div>
</div>
<script>
// 搜索引擎配置
const searchEngines = {
baidu: "https://www.baidu.com/s?wd=",
bing: "https://www.bing.com/search?q=",
sm: "https://m.sm.cn/s?q=",
toutiao: "https://so.toutiao.com/search?dvpf=pc&source=trending_card&keyword=",
bilibili: "https://search.bilibili.com/all?keyword=",
github: "https://github.com/search?q=",
google: "https://www.google.com/search?q=",
yandex: "https://www.yandex.com/search/?text="
};
let currentEngine = "baidu";
// 日志记录函数
function logAction(action, details) {
const timestamp = new Date().toISOString();
const logEntry = timestamp + ': ' + action + ' - ' + JSON.stringify(details);
let logs = JSON.parse(localStorage.getItem('cardTabLogs') || '[]');
logs.push(logEntry);
// 保留最新的1000条日志
if (logs.length > 1000) {
logs = logs.slice(-1000);
}
localStorage.setItem('cardTabLogs', JSON.stringify(logs));
console.log(logEntry); // 同时在控制台输出日志
}
// 查看日志的函数
function viewLogs() {
const logs = JSON.parse(localStorage.getItem('cardTabLogs') || '[]');
console.log('Card Tab Logs:');
logs.forEach(log => console.log(log));
alert('日志已在控制台输出,请按F12打开开发者工具查看。');
}
// 设置当前搜索引擎
function setActiveEngine(engine) {
currentEngine = engine;
document.querySelectorAll('.search-engine').forEach(btn => {
btn.style.backgroundColor = btn.dataset.engine === engine ? '#c0c0c0' : '#f0f0f0';
});
logAction('设置搜索引擎', { engine });
}
// 搜索引擎按钮点击事件
document.querySelectorAll('.search-engine').forEach(button => {
button.addEventListener('click', () => setActiveEngine(button.dataset.engine));
});
// 搜索按钮点击事件
document.getElementById('search-button').addEventListener('click', () => {
const query = document.getElementById('search-input').value;
if (query) {
logAction('执行搜索', { engine: currentEngine, query });
window.open(searchEngines[currentEngine] + encodeURIComponent(query), '_blank');
}
});
// 搜索输入框回车事件
document.getElementById('search-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
document.getElementById('search-button').click();
}
});
// 初始化搜索引擎
setActiveEngine(currentEngine);
// 全局变量
let publicLinks = [];
let privateLinks = [];
let isAdmin = false;
let isLoggedIn = false;
let removeMode = false;
let isRemoveCategoryMode = false;
let isDarkTheme = false;
let links = [];
const categories = {};
// 添加新分类
function addCategory() {
const categoryName = prompt('请输入新分类名称:');
if (categoryName && !categories[categoryName]) {
categories[categoryName] = [];
updateCategorySelect();
renderCategories();
saveLinks();
logAction('添加分类', { categoryName, currentLinkCount: links.length });
} else if (categories[categoryName]) {
alert('该分类已存在');
logAction('添加分类失败', { categoryName, reason: '分类已存在' });
}
}
// 删除分类
function deleteCategory(category) {
if (confirm('确定要删除 "' + category + '" 分类吗?这将删除该分类下的所有链接。')) {
delete categories[category];
links = links.filter(link => link.category !== category);
publicLinks = publicLinks.filter(link => link.category !== category);
privateLinks = privateLinks.filter(link => link.category !== category);
updateCategorySelect();
saveLinks();
renderCategories();
logAction('删除分类', { category });
}
}
// 渲染分类(不重新加载链接)
function renderCategories() {
const container = document.getElementById('sections-container');
container.innerHTML = '';
Object.keys(categories).forEach(category => {
const section = document.createElement('div');
section.className = 'section';
const titleContainer = document.createElement('div');
titleContainer.className = 'section-title-container';
const title = document.createElement('div');
title.className = 'section-title';
title.textContent = category;
titleContainer.appendChild(title);
if (isAdmin) {
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '删除分类';
deleteBtn.className = 'delete-category-btn';
deleteBtn.style.display = isRemoveCategoryMode ? 'inline-block' : 'none';
deleteBtn.onclick = () => deleteCategory(category);
titleContainer.appendChild(deleteBtn);
}
const cardContainer = document.createElement('div');
cardContainer.className = 'card-container';
cardContainer.id = category;
section.appendChild(titleContainer);
section.appendChild(cardContainer);
container.appendChild(section);
// 只渲染属于当前分类的链接
const categoryLinks = links.filter(link => link.category === category);
categoryLinks.forEach(link => {
createCard(link, cardContainer);
});
});
logAction('渲染分类', { categoryCount: Object.keys(categories).length, linkCount: links.length });
}
// 读取链接数据
async function loadLinks() {
const response = await fetch('/api/getLinks?userId=testUser');
const data = await response.json();
if (data.categories) {
Object.assign(categories, data.categories);
}
publicLinks = data.links ? data.links.filter(link => !link.isPrivate) : [];
privateLinks = data.links ? data.links.filter(link => link.isPrivate) : [];
links = isLoggedIn ? [...publicLinks, ...privateLinks] : publicLinks;
loadSections();
updateCategorySelect();
updateUIState();
logAction('读取链接', { publicCount: publicLinks.length, privateCount: privateLinks.length });
}
// 更新UI状态
function updateUIState() {
const passwordInput = document.getElementById('admin-password');
const adminBtn = document.getElementById('admin-mode-btn');
const secretGardenBtn = document.getElementById('secret-garden-btn');
const addRemoveControls = document.querySelector('.add-remove-controls');
passwordInput.style.display = isLoggedIn ? 'none' : 'inline-block';
secretGardenBtn.textContent = isLoggedIn ? "退出" : "登录";
secretGardenBtn.style.display = 'inline-block';
if (isAdmin) {
adminBtn.textContent = "离开设置";
adminBtn.style.display = 'inline-block';
addRemoveControls.style.display = 'flex';
} else if (isLoggedIn) {
adminBtn.textContent = "设置";
adminBtn.style.display = 'inline-block';
addRemoveControls.style.display = 'none';
} else {
adminBtn.style.display = 'none';
addRemoveControls.style.display = 'none';
}
logAction('更新UI状态', { isAdmin, isLoggedIn });
}
// 登录状态显示(加载所有链接)
function showSecretGarden() {
if (isLoggedIn) {
links = [...publicLinks, ...privateLinks];
loadSections();
// 显示所有私密标签
document.querySelectorAll('.private-tag').forEach(tag => {
tag.style.display = 'block';
});
logAction('显示私密花园');
}
}
// 加载分类和链接
function loadSections() {
const container = document.getElementById('sections-container');
container.innerHTML = '';
Object.keys(categories).forEach(category => {
const section = document.createElement('div');
section.className = 'section';
const titleContainer = document.createElement('div');
titleContainer.className = 'section-title-container';
const title = document.createElement('div');
title.className = 'section-title';
title.textContent = category;
titleContainer.appendChild(title);
if (isAdmin) {
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '删除分类';
deleteBtn.className = 'delete-category-btn';
deleteBtn.style.display = 'none';
deleteBtn.onclick = () => deleteCategory(category);
titleContainer.appendChild(deleteBtn);
}
const cardContainer = document.createElement('div');
cardContainer.className = 'card-container';
cardContainer.id = category;
section.appendChild(titleContainer);
section.appendChild(cardContainer);
let privateCount = 0;
let linkCount = 0;
links.forEach(link => {
if (link.category === category) {
if (link.isPrivate) privateCount++;
linkCount++;
createCard(link, cardContainer);
}
});
if (privateCount < linkCount || isLoggedIn) {
container.appendChild(section);
}
});
logAction('加载分类和链接', { isAdmin: isAdmin, linkCount: links.length, categoryCount: Object.keys(categories).length });
}
// 创建卡片
function createCard(link, container) {
const card = document.createElement('div');
card.className = 'card';
card.setAttribute('draggable', isAdmin);
card.dataset.isPrivate = link.isPrivate;
const cardTop = document.createElement('div');
cardTop.className = 'card-top';
const icon = document.createElement('img');
icon.className = 'card-icon';
icon.src = 'https://favicon.zhusl.com/ico?url=' + link.url;
icon.alt = 'Website Icon';
const title = document.createElement('div');
title.className = 'card-title';
title.textContent = link.name;
cardTop.appendChild(icon);
cardTop.appendChild(title);
const url = document.createElement('div');
url.className = 'card-url';
url.textContent = link.url;
card.appendChild(cardTop);
card.appendChild(url);
if (link.isPrivate) {
const privateTag = document.createElement('div');
privateTag.className = 'private-tag';
privateTag.textContent = '私密';
card.appendChild(privateTag);
}
const correctedUrl = link.url.startsWith('http://') || link.url.startsWith('https://') ? link.url : 'http://' + link.url;
if (!isAdmin) {
card.addEventListener('click', () => {
window.open(correctedUrl, '_blank');
logAction('打开链接', { name: link.name, url: correctedUrl });
});
}
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '–';
deleteBtn.className = 'delete-btn';
deleteBtn.onclick = function (event) {
event.stopPropagation();
removeCard(card);
};
card.appendChild(deleteBtn);
updateCardStyle(card);
card.addEventListener('dragstart', dragStart);
card.addEventListener('dragover', dragOver);
card.addEventListener('dragend', dragEnd);
card.addEventListener('drop', drop);
if (isAdmin && removeMode) {
deleteBtn.style.display = 'block';
}
if (isAdmin || (link.isPrivate && isLoggedIn) || !link.isPrivate) {
container.appendChild(card);
}
// logAction('创建卡片', { name: link.name, isPrivate: link.isPrivate });
}
// 更新卡片样式
function updateCardStyle(card) {
if (isDarkTheme) {
card.style.backgroundColor = '#1e1e1e';
card.style.color = '#ffffff';
card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)';
} else {
card.style.backgroundColor = '#a0c9e5';
card.style.color = '#333';
card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)';
}
}
// 更新分类选择下拉框
function updateCategorySelect() {
const categorySelect = document.getElementById('category-select');
categorySelect.innerHTML = '';
Object.keys(categories).forEach(category => {
const option = document.createElement('option');
option.value = category;
option.textContent = category;
categorySelect.appendChild(option);
});
logAction('更新分类选择', { categoryCount: Object.keys(categories).length });
}
// 保存链接数据
async function saveLinks() {
let allLinks = [...publicLinks, ...privateLinks];
try {
await fetch('/api/saveOrder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: 'testUser',
links: allLinks,
categories: categories
}),
});
logAction('保存链接', { linkCount: allLinks.length, categoryCount: Object.keys(categories).length });
} catch (error) {
console.error('Error saving links:', error);
logAction('保存链接失败', { error: error.message });
alert('保存链接失败,请重试');
}
}
// 添加卡片弹窗
function addLink() {
const name = document.getElementById('name-input').value;
const url = document.getElementById('url-input').value;
const category = document.getElementById('category-select').value;
const isPrivate = document.getElementById('private-checkbox').checked;
if (name && url && category) {
const newLink = { name, url, category, isPrivate };
if (isPrivate) {
privateLinks.push(newLink);
} else {
publicLinks.push(newLink);
}
links = isLoggedIn ? [...publicLinks, ...privateLinks] : publicLinks;
if (isAdmin || (isPrivate && isLoggedIn) || !isPrivate) {
const container = document.getElementById(category);
if (container) {
createCard(newLink, container);
} else {
categories[category] = [];
renderCategories();
}
}
saveLinks();
document.getElementById('name-input').value = '';
document.getElementById('url-input').value = '';
document.getElementById('private-checkbox').checked = false;
hideAddDialog();
logAction('添加卡片', { name, url, category, isPrivate });
}
}
// 删除卡片
function removeCard(card) {
const name = card.querySelector('.card-title').textContent;
const url = card.querySelector('.card-url').textContent;
const isPrivate = card.dataset.isPrivate === 'true';
links = links.filter(link => link.url !== url);
if (isPrivate) {
privateLinks = privateLinks.filter(link => link.url !== url);
} else {
publicLinks = publicLinks.filter(link => link.url !== url);
}
for (const key in categories) {
categories[key] = categories[key].filter(link => link.url !== url);
}
card.remove();
saveLinks();
logAction('删除卡片', { name, url, isPrivate });
}
// 拖拽卡片
let draggedCard = null;
function dragStart(event) {
if (!isAdmin) return;
draggedCard = event.target;
draggedCard.classList.add('dragging');
event.dataTransfer.effectAllowed = "move";
logAction('开始拖拽卡片', { name: draggedCard.querySelector('.card-title').textContent });
}
function dragOver(event) {
if (!isAdmin) return;
event.preventDefault();
const target = event.target.closest('.card');
if (target && target !== draggedCard) {
const container = target.parentElement;
const mousePositionX = event.clientX;
const targetRect = target.getBoundingClientRect();
if (mousePositionX < targetRect.left + targetRect.width /2) {
container.insertBefore(draggedCard, target);
} else {
container.insertBefore(draggedCard, target.nextSibling);
}
}
//logAction('拖拽卡片中', { over: target ? target.querySelector('.card-title').textContent : 'unknown' });
}
function drop(event) {
if (!isAdmin) return;
event.preventDefault();
draggedCard.classList.remove('dragging');
const targetCategory = event.target.closest('.card-container').id;
// 更新卡片的分类信息
const cardTitle = draggedCard.querySelector('.card-title').textContent;
const cardUrl = draggedCard.querySelector('.card-url').textContent;
const isPrivate = draggedCard.dataset.isPrivate === 'true';
// 在 links 数组中更新卡片信息
const linkIndex = links.findIndex(link => link.url === cardUrl);
if (linkIndex !== -1) {
links[linkIndex].category = targetCategory;
}
// 在 publicLinks 或 privateLinks 中更新卡片信息
const linkArray = isPrivate ? privateLinks : publicLinks;
const arrayIndex = linkArray.findIndex(link => link.url === cardUrl);
if (arrayIndex !== -1) {
linkArraycategory = targetCategory;
}
draggedCard.dataset.category = targetCategory;
logAction('放下卡片', { name: cardTitle, category: targetCategory });
draggedCard = null;
saveCardOrder();
}
function dragEnd(event) {
if (draggedCard) {
draggedCard.classList.remove('dragging');
logAction('拖拽卡片结束');
}
}
// 保存卡片顺序
async function saveCardOrder() {
if (!isAdmin) return;
const containers = document.querySelectorAll('.card-container');
let newPublicLinks = [];
let newPrivateLinks = [];
let newCategories = {};
containers.forEach(container => {
const category = container.id;
newCategories[category] = [];
[...container.children].forEach(card => {
const url = card.querySelector('.card-url').textContent;
const name = card.querySelector('.card-title').textContent;
const isPrivate = card.dataset.isPrivate === 'true';
card.dataset.category = category;
const link = { name, url, category, isPrivate };
if (isPrivate) {
newPrivateLinks.push(link);
} else {
newPublicLinks.push(link);
}
newCategories[category].push(link);
});
});
publicLinks.length = 0;
publicLinks.push(...newPublicLinks);
privateLinks.length = 0;
privateLinks.push(...newPrivateLinks);
Object.keys(categories).forEach(key => delete categories[key]);
Object.assign(categories, newCategories);
logAction('保存卡片顺序', {
publicCount: newPublicLinks.length,
privateCount: newPrivateLinks.length,
categoryCount: Object.keys(newCategories).length
});
try {
const response = await fetch('/api/saveOrder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: 'testUser',
links: [...newPublicLinks, ...newPrivateLinks],
categories: newCategories
}),
});
const result = await response.json();
if (!result.success) {
throw new Error('Failed to save order');
}
logAction('保存卡片顺序', { publicCount: newPublicLinks.length, privateCount: newPrivateLinks.length, categoryCount: Object.keys(newCategories).length });
} catch (error) {
console.error('Error saving order:', error);
logAction('保存顺序失败', { error: error.message });
alert('保存顺序失败,请重试');
}
}
// 设置状态重新加载卡片
function reloadCardsAsAdmin() {
document.querySelectorAll('.card-container').forEach(container => {
container.innerHTML = '';
});
loadLinks().then(() => {
if (isDarkTheme) {
applyDarkTheme();
}
});
logAction('重新加载卡片(管理员模式)');
}
// 密码输入框回车事件
document.getElementById('admin-password').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
toggleSecretGarden();
}
});
// 切换设置状态
function toggleAdminMode() {
const adminBtn = document.getElementById('admin-mode-btn');
const addRemoveControls = document.querySelector('.add-remove-controls');
if (!isAdmin && isLoggedIn) {
isAdmin = true;
adminBtn.textContent = "退出设置";
addRemoveControls.style.display = 'flex';
alert('准备设置分类和书签');
reloadCardsAsAdmin();
logAction('进入设置');
} else if (isAdmin) {
isAdmin = false;
removeMode = false;
adminBtn.textContent = "设 置";
addRemoveControls.style.display = 'none';
alert('设置已保存');
reloadCardsAsAdmin();
logAction('离开');
}
updateUIState();
}
// 切换到登录状态
function toggleSecretGarden() {
const passwordInput = document.getElementById('admin-password');
if (!isLoggedIn) {
verifyPassword(passwordInput.value).then(isValid => {
if (isValid) {
isLoggedIn = true;
links = [...publicLinks, ...privateLinks];
loadSections();
alert('登录成功!');
logAction('登录成功');
} else {
alert('密码错误');
logAction('登录失败', { reason: '密码错误' });
}
updateUIState();
});
} else {
isLoggedIn = false;
isAdmin = false;
links = publicLinks;
loadSections();
alert('退出登录!');
updateUIState();
passwordInput.value = '';
logAction('退出登录');
}
}
// 应用暗色主题
function applyDarkTheme() {
document.body.style.backgroundColor = '#121212';
document.body.style.color = '#ffffff';
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
card.style.backgroundColor = '#1e1e1e';
card.style.color = '#ffffff';
card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)';
});
logAction('应用暗色主题');
}
// 显示添加链接对话框
function showAddDialog() {
document.getElementById('dialog-overlay').style.display = 'flex';
logAction('显示添加链接对话框');
}
// 隐藏添加链接对话框
function hideAddDialog() {
document.getElementById('dialog-overlay').style.display = 'none';
logAction('隐藏添加链接对话框');
}
// 切换删除卡片模式
function toggleRemoveMode() {
removeMode = !removeMode;
const deleteButtons = document.querySelectorAll('.delete-btn');
deleteButtons.forEach(btn => {
btn.style.display = removeMode ? 'block' : 'none';
});
logAction('切换删除卡片模式', { removeMode });
}
//切换删除分类模式
function toggleRemoveCategory() {
isRemoveCategoryMode = !isRemoveCategoryMode;
const deleteButtons = document.querySelectorAll('.delete-category-btn');
deleteButtons.forEach(btn => {
btn.style.display = isRemoveCategoryMode ? 'inline-block' : 'none';
});
logAction('切换删除分类模式', { isRemoveCategoryMode });
}
// 切换主题
function toggleTheme() {
isDarkTheme = !isDarkTheme;
document.body.style.backgroundColor = isDarkTheme ? '#121212' : '#ffffff';
document.body.style.color = isDarkTheme ? '#ffffff' : '#333';
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
card.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#a0c9e5';
card.style.color = isDarkTheme ? '#ffffff' : '#333';
card.style.boxShadow = isDarkTheme
? '0 4px 8px rgba(0, 0, 0, 0.5)'
: '0 4px 8px rgba(0, 0, 0, 0.1)';
});
const fixedElements = document.querySelectorAll('.fixed-elements');
fixedElements.forEach(element => {
element.style.backgroundColor = isDarkTheme ? '#121212' : '#ffffff';
element.style.color = isDarkTheme ? '#ffffff' : '#333';
});
const dialogBox = document.getElementById('dialog-box');
dialogBox.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#ffffff';
dialogBox.style.color = isDarkTheme ? '#ffffff' : '#333';
const inputs = document.querySelectorAll('input[type="text"], input[type="password"], select');
inputs.forEach(input => {
input.style.backgroundColor = isDarkTheme ? '#444' : '#fff';
input.style.color = isDarkTheme ? '#fff' : '#333';
input.style.borderColor = isDarkTheme ? '#555' : '#ccc';
});
logAction('切换主题', { isDarkTheme });
}
// 验证密码
async function verifyPassword(inputPassword) {
const response = await fetch('/api/verifyPassword', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: inputPassword }),
});
const result = await response.json();
return result.valid;
}
// 初始化加载链接
loadLinks();
</script>
</body>
</html>
`;
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === '/') {
return new Response(HTML_CONTENT, {
headers: { 'Content-Type': 'text/html' }
});
}
if (url.pathname === '/api/getLinks') {
const userId = url.searchParams.get('userId');
const links = await env.CARD_ORDER.get(userId);
return new Response(links || JSON.stringify([]), { status: 200 });
}
if (url.pathname === '/api/saveOrder' && request.method === 'POST') {
const { userId, links, categories } = await request.json();
await env.CARD_ORDER.put(userId, JSON.stringify({ links, categories })); //保存链接和分类
return new Response(JSON.stringify({ success: true }), { status: 200 });
}
if (url.pathname === '/api/verifyPassword' && request.method === 'POST') {
const { password } = await request.json();
const isValid = password === env.ADMIN_PASSWORD; // 从环境变量中获取密码
return new Response(JSON.stringify({ valid: isValid }), {
status: isValid ? 200 : 403,
headers: { 'Content-Type': 'application/json' },
});
}
return new Response('Not Found', { status: 404 });
}
};
旧版本
1、原workes, 效果
const HTML_CONTENT = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Card Tab</title>
<style>
body {
font-family: Arial, sans-serif;
// background-color: #f4f4f4;
background-color: #d8eac4;
margin: 0;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
transition: background-color 0.3s ease;
}
.card-container {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 10px;
}
.card {
display: flex;
flex-direction: column;
position: relative;
background-color: #a0c9e5;
padding: 10px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
cursor: grab;
transition: transform 0.2s ease, box-shadow 0.2s ease;
width: 200px;
height: auto;
}
.card-top {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.card-icon {
width: 24px;
height: 24px;
margin-right: 10px;
}
.card-title {
font-size: 16px;
font-weight: bold;
}
.card-url {
color: #555;
font-size: 12px;
word-break: break-all;
}
.card.dragging {
opacity: 0.8;
transform: scale(1.05);
cursor: grabbing;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
}
.delete-btn {
position: absolute;
top: -10px;
right: -10px;
background-color: red;
color: white;
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
text-align: center;
font-size: 14px;
line-height: 20px;
cursor: pointer;
display: none;
}
.admin-controls {
position: fixed;
top: 10px;
right: 10px;
font-size: 60%;
}
.admin-controls input {
padding: 5px;
font-size: 60%;
}
.admin-controls button {
padding: 5px 10px;
font-size: 60%;
margin-left: 10px;
}
.add-remove-controls {
display: none;
margin-top: 10px;
}
.round-btn {
background-color: #007bff;
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
text-align: center;
font-size: 24px;
line-height: 40px;
cursor: pointer;
margin: 0 10px;
}
#theme-toggle {
position: fixed;
bottom: 10px;
left: 10px;
background-color: #007bff;
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
text-align: center;
font-size: 24px;
line-height: 40px;
cursor: pointer;
}
#dialog-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
}
#dialog-box {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
#dialog-box label {
display: block;
margin-bottom: 5px;
}
#dialog-box input, #dialog-box select {
width: 100%;
padding: 5px;
margin-bottom: 10px;
}
#dialog-box button {
padding: 5px 10px;
margin-right: 10px;
}
.section {
margin-bottom: 20px;
}
.section-title {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 10px;
}
</style>
</head>
<body>
<h1>我的导航</h1>
<div class="admin-controls">
<input type="password" id="admin-password" placeholder="输入密码">
<button id="admin-mode-btn" onclick="toggleAdminMode()">进入管理模式</button>
</div>
<div class="add-remove-controls">
<button class="round-btn" onclick="showAddDialog()">+</button>
<button class="round-btn" onclick="toggleRemoveMode()">-</button>
</div>
<div id="sections-container">
<!-- 分类将在这里动态生成 -->
</div>
<button id="theme-toggle" onclick="toggleTheme()">◑</button>
<div id="dialog-overlay">
<div id="dialog-box">
<label for="name-input">名称</label>
<input type="text" id="name-input">
<label for="url-input">地址</label>
<input type="text" id="url-input">
<label for="category-select">选择分类</label>
<select id="category-select">
<!-- 分类选项将在这里动态生成 -->
</select>
<button onclick="addLink()">确定</button>
<button onclick="hideAddDialog()">取消</button>
</div>
</div>
<div class="copyright">
<!-- 请不要删除 -->
<p> 项目地址: <a href="https://github.com/hmhm2022/Card-Tab" target="_blank">GitHub</a> 烦请点个star!
</div>
<script>
let isAdmin = false;
let removeMode = false;
let isDarkTheme = false;
let links = [];
const categories = {
"常用网站": [], // **编辑自己的网站分类**
"工具导航": [],
"游戏娱乐": [],
"影音视听": [],
"技术论坛": []
};
async function loadLinks() {
const response = await fetch('/api/getLinks?userId=testUser');
links = await response.json();
Object.keys(categories).forEach(key => {
categories[key] = [];
});
links.forEach(link => {
if (categories[link.category]) {
categories[link.category].push(link);
}
});
loadSections();
updateCategorySelect();
// applyTheme();
}
function loadSections() {
const container = document.getElementById('sections-container');
container.innerHTML = '';
Object.keys(categories).forEach(category => {
const section = document.createElement('div');
section.className = 'section';
const title = document.createElement('div');
title.className = 'section-title';
title.textContent = category;
const cardContainer = document.createElement('div');
cardContainer.className = 'card-container';
cardContainer.id = category;
section.appendChild(title);
section.appendChild(cardContainer);
categories[category].forEach(link => {
createCard(link, cardContainer);
});
container.appendChild(section);
});
}
function createCard(link, container) {
const card = document.createElement('div');
card.className = 'card';
card.setAttribute('draggable', isAdmin);
const cardTop = document.createElement('div');
cardTop.className = 'card-top';
const icon = document.createElement('img');
icon.className = 'card-icon';
// icon.src = 'https://www.google.com/s2/favicons?domain=' + link.url;
icon.src = 'https://favicon.zhusl.com/ico?url=' + link.url;
icon.alt = 'Website Icon';
const title = document.createElement('div');
title.className = 'card-title';
title.textContent = link.name;
cardTop.appendChild(icon);
cardTop.appendChild(title);
const url = document.createElement('div');
url.className = 'card-url';
url.textContent = link.url;
card.appendChild(cardTop);
card.appendChild(url);
// URL 检查和修正
function correctUrl(url) {
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
} else {
return 'http://' + url;
}
}
let correctedUrl = correctUrl(link.url);
if (!isAdmin) {
card.addEventListener('click', () => {
window.open(correctedUrl, '_blank');
});
}
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '–';
deleteBtn.className = 'delete-btn';
deleteBtn.onclick = function (event) {
event.stopPropagation();
removeCard(card);
};
card.appendChild(deleteBtn);
if (isDarkTheme) {
card.style.backgroundColor = '#1e1e1e';
card.style.color = '#ffffff';
card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)';
} else {
card.style.backgroundColor = '#a0c9e5';
card.style.color = '#333';
card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)';
}
card.addEventListener('dragstart', dragStart);
card.addEventListener('dragover', dragOver);
card.addEventListener('dragend', dragEnd);
card.addEventListener('drop', drop);
if (isAdmin && removeMode) {
deleteBtn.style.display = 'block';
}
container.appendChild(card);
}
function updateCategorySelect() {
const categorySelect = document.getElementById('category-select');
categorySelect.innerHTML = '';
Object.keys(categories).forEach(category => {
const option = document.createElement('option');
option.value = category;
option.textContent = category;
categorySelect.appendChild(option);
});
}
async function saveLinks() {
let links = [];
for (const category in categories) {
links = links.concat(categories[category]);
}
await fetch('/api/saveOrder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: 'testUser', links }),
});
}
function addLink() {
const name = document.getElementById('name-input').value;
const url = document.getElementById('url-input').value;
const category = document.getElementById('category-select').value;
if (name && url && category) {
const newLink = { name, url, category };
if (!categories[category]) {
categories[category] = [];
}
categories[category].push(newLink);
const container = document.getElementById(category);
createCard(newLink, container);
saveLinks();
document.getElementById('name-input').value = '';
document.getElementById('url-input').value = '';
hideAddDialog();
}
}
function removeCard(card) {
const url = card.querySelector('.card-url').textContent;
let category;
for (const key in categories) {
const index = categories[key].findIndex(link => link.url === url);
if (index !== -1) {
categories[key].splice(index, 1);
category = key;
break;
}
}
card.remove();
saveLinks();
}
let draggedCard = null;
function dragStart(event) {
if (!isAdmin) return;
draggedCard = event.target;
draggedCard.classList.add('dragging');
event.dataTransfer.effectAllowed = "move";
}
function dragOver(event) {
if (!isAdmin) return;
event.preventDefault();
const target = event.target.closest('.card');
if (target && target !== draggedCard) {
const container = target.parentElement;
const mousePositionX = event.clientX;
const targetRect = target.getBoundingClientRect();
if (mousePositionX < targetRect.left + targetRect.width / 2) {
container.insertBefore(draggedCard, target);
} else {
container.insertBefore(draggedCard, target.nextSibling);
}
}
}
function drop(event) {
if (!isAdmin) return;
event.preventDefault();
draggedCard.classList.remove('dragging');
draggedCard = null;
saveCardOrder();
}
// function dragEnd(event) {
// draggedCard.classList.remove('dragging');
function dragEnd(event) {
if (draggedCard) {
draggedCard.classList.remove('dragging');
}
}
async function saveCardOrder() {
if (!isAdmin) return;
const containers = document.querySelectorAll('.card-container');
let newLinks = [];
containers.forEach(container => {
const category = container.id;
categories[category] = [];
[...container.children].forEach(card => {
const url = card.querySelector('.card-url').textContent;
const name = card.querySelector('.card-title').textContent;
const link = { name, url, category };
categories[category].push(link);
newLinks.push(link);
});
});
links = newLinks;
await fetch('/api/saveOrder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: 'testUser', links: newLinks }),
});
}
function toggleAdminMode() {
const passwordInput = document.getElementById('admin-password');
const adminBtn = document.getElementById('admin-mode-btn');
const addRemoveControls = document.querySelector('.add-remove-controls');
if (!isAdmin) {
verifyPassword(passwordInput.value)
.then(isValid => {
if (isValid) {
isAdmin = true;
adminBtn.textContent = "退出管理模式";
alert('已进入管理模式');
addRemoveControls.style.display = 'block';
reloadCardsAsAdmin();
} else {
alert('密码错误');
}
});
} else {
isAdmin = false;
removeMode = false;
adminBtn.textContent = "进入管理模式";
alert('已退出管理模式');
addRemoveControls.style.display = 'none';
const deleteButtons = document.querySelectorAll('.delete-btn');
deleteButtons.forEach(btn => btn.style.display = 'none');
reloadCardsAsAdmin();
}
passwordInput.value = '';
}
function reloadCardsAsAdmin() {
document.querySelectorAll('.card-container').forEach(container => {
container.innerHTML = '';
});
loadLinks().then(() => {
if (isDarkTheme) {
applyDarkTheme();
}
});
}
function applyDarkTheme() {
document.body.style.backgroundColor = '#121212';
document.body.style.color = '#ffffff';
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
card.style.backgroundColor = '#1e1e1e';
card.style.color = '#ffffff';
card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)';
});
}
function showAddDialog() {
document.getElementById('dialog-overlay').style.display = 'flex';
}
function hideAddDialog() {
document.getElementById('dialog-overlay').style.display = 'none';
}
function toggleRemoveMode() {
removeMode = !removeMode;
const deleteButtons = document.querySelectorAll('.delete-btn');
deleteButtons.forEach(btn => {
btn.style.display = removeMode ? 'block' : 'none';
});
}
function toggleTheme() {
isDarkTheme = !isDarkTheme;
// 设置暗色主题和亮色主题的背景色
document.body.style.backgroundColor = isDarkTheme ? '#121212' : '#d8eac4';
// 设置暗色主题和亮色主题的文本颜色
document.body.style.color = isDarkTheme ? '#ffffff' : '#333';
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
// 卡片背景和文本颜色设置
card.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#a0c9e5';
card.style.color = isDarkTheme ? '#ffffff' : '#333';
// 卡片阴影的设置,增强暗色主题的阴影
card.style.boxShadow = isDarkTheme
? '0 4px 8px rgba(0, 0, 0, 0.5)'
: '0 4px 8px rgba(0, 0, 0, 0.1)';
});
const dialogBox = document.getElementById('dialog-box');
// 对话框背景和文本颜色设置
dialogBox.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#ffffff';
dialogBox.style.color = isDarkTheme ? '#ffffff' : '#333';
const inputs = dialogBox.querySelectorAll('input, select');
inputs.forEach(input => {
// 输入框背景和文本颜色设置
input.style.backgroundColor = isDarkTheme ? '#333333' : '#ffffff';
input.style.color = isDarkTheme ? '#ffffff' : '#333';
});
}
async function verifyPassword(inputPassword) {
const response = await fetch('/api/verifyPassword', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: inputPassword }),
});
const result = await response.json();
return result.valid;
}
loadLinks();
</script>
</body>
</html>
`;
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === '/') {
return new Response(HTML_CONTENT, {
headers: { 'Content-Type': 'text/html' }
});
}
if (url.pathname === '/api/getLinks') {
const userId = url.searchParams.get('userId');
const links = await env.CARD_ORDER.get(userId);
return new Response(links || JSON.stringify([]), { status: 200 });
}
if (url.pathname === '/api/saveOrder' && request.method === 'POST') {
const { userId, links } = await request.json();
await env.CARD_ORDER.put(userId, JSON.stringify(links));
return new Response(JSON.stringify({ success: true }), { status: 200 });
}
if (url.pathname === '/api/verifyPassword' && request.method === 'POST') {
const { password } = await request.json();
const isValid = password === env.ADMIN_PASSWORD; // 从环境变量中获取密码
return new Response(JSON.stringify({ valid: isValid }), {
status: isValid ? 200 : 403,
headers: { 'Content-Type': 'application/json' },
});
}
return new Response('Not Found', { status: 404 });
}
};
2、修改后的workes, 效果
const HTML_CONTENT = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WS01の主页</title>
<style>
body {
font-family: Arial, sans-serif;
// background-color: #f4f4f4;
background-color: #d4d4d4;
margin: 0;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
transition: background-color 0.3s ease;
}
ul {
padding: 0;
margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 0px;
margin-inline-end: 0px;
padding-inline-start: 40px;
unicode-bidi: isolate;
}
li {
display: list-item;
text-align: -webkit-match-parent;
unicode-bidi: isolate;
margin: 0 8px;
}
.background {
background-image: linear-gradient(#d4d4d4 1px, transparent 0), linear-gradient(90deg, #d4d4d4 1px, transparent 0);
background-size: 32px 32px;
background-color: #fffcf8;
}
.header {
background-color: #fff;
box-shadow: 0 0 5px rgba(0, 0, 0, .1);
transition: background-color .5s;
}
.navbar {
display: flex;
width: 1280px;
margin: auto;
height: 40px;
}
.navbar .brand {
display: flex;
align-items: center;
color: #555;
}
.brand .logo {
max-width: 36px;
}
.brand .title {
margin-left: 5px;
font-family: helvetica neue, helvetica, arial, sans-serif;
font-size: 24px;
font-weight: 700;
}
.beta {
color: #ccc;
font-size: 11px;
font-weight: 400;
position: relative;
top: -14px;
}
.category-list {
list-style: none;
display: flex;
align-items: center;
}
.sites {
width:1280px;
background:;
border:2px solid auto;
margin:15px auto;
padding:0px;
text-align:center;
}
.sites1 {
width:1280px;
background:;
border:2px solid auto;
margin:15px auto;
padding:0px;
}
.sites dl {
height:36px;
line-height:36px;
display:block;
margin:0;
}
.sites dl.alt {
background:#d4d4d4;
border-top:1px solid #ffffff;
border-bottom:1px solid #ffffff;
}
.sites dl.alt2 {
background:#d4d4d4;
border-top:1px solid #ffffff;
border-bottom:1px solid #ffffff;
}
.sites dt,.sites dd {
text-align:center;
display:block;
float:left;
}
.sites dt {
width:60px;
}
.sites dd {
width:90px;
margin:0;
}
.footer {
width:580px;
text-align:center;
margin:5px auto;
padding:2px;
}
.card-container {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 6px;
width: 100%; /* 宽度设为100%,以适应不同设备 */
}
.card {
display: flex;
flex-direction: column;
position: relative;
background-color: #d4d4d4;
padding: 10px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
cursor: grab;
transition: transform 0.2s ease, box-shadow 0.2s ease;
word-break: break-word; /* 防止超出边界 */
}
/* 中等屏幕 (平板等) */
@media (max-width: 1024px) {
.card-container {
grid-template-columns: repeat(8, 1fr); /* 中等屏幕显示4列 */
}
}
/* 小屏幕 (手机) */
@media (max-width: 768px) {
.card-container {
grid-template-columns: repeat(6, 1fr); /* 小屏幕显示2列 */
}
.card {
padding: 8px; /* 减小卡片内边距 */
font-size: 0.9rem; /* 调整字体大小0.9 */
}
.admin-controls {
top: 5px;
right: 5px;
}
.round-btn, #theme-toggle {
width: 30px;
height: 30px;
font-size: 18px;
line-height: 30px;
}
}
/* 超小屏幕 (更小手机) */
@media (max-width: 480px) {
.card-container {
grid-template-columns: repeat(5, 1fr); /* 超小屏幕显示1列 */
}
.card {
padding: 6px; /* 进一步减小卡片内边距5 */
font-size: 0.8rem; /* 再次缩小字体0.8 */
}
.round-btn, #theme-toggle {
width: 25px;
height: 25px;
font-size: 16px;
line-height: 25px;
}
}
.card-top {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.card-icon {
width: 20px;
height: 20px;
margin-right: 8px;
}
.card-title {
font-size: 16px;
font-weight: bold;
}
.card-url {
color: #555;
font-size: 12px;
word-break: break-all;
}
.card.dragging {
opacity: 0.8;
transform: scale(1.05);
cursor: grabbing;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.delete-btn {
position: absolute;
top: -10px;
right: -10px;
background-color: red;
color: white;
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
text-align: center;
font-size: 14px;
line-height: 20px;
cursor: pointer;
display: none;
}
.admin-controls {
position: fixed;
top: 10px;
right: 10px;
font-size: 60%;
}
.admin-controls input {
padding: 5px;
font-size: 60%;
}
.admin-controls button {
padding: 5px 10px;
font-size: 60%;
margin-left: 10px;
}
.add-remove-controls {
display: none;
margin-top: 10px;
}
.round-btn {
background-color: #007bff;
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
text-align: center;
font-size: 24px;
line-height: 40px;
cursor: pointer;
margin: 0 10px;
}
#theme-toggle {
position: fixed;
bottom: 10px;
left: 10px;
background-color: #007bff;
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
text-align: center;
font-size: 24px;
line-height: 40px;
cursor: pointer;
}
#dialog-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
}
#dialog-box {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
#dialog-box label {
display: block;
margin-bottom: 5px;
}
#dialog-box input, #dialog-box select {
width: 100%;
padding: 5px;
margin-bottom: 10px;
}
#dialog-box button {
padding: 5px 10px;
margin-right: 10px;
}
.section {
margin-bottom: 20px;
}
.section-title {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 10px;
}
</style>
</head>
<body class="background">
</div>
<div class="sites"> <!-- 00 -->
<header class="header">
<nav class="navbar">
<a href="https://ddo.us.kg/" class="brand">
<img class="debug logo" src="https://cdn.glitch.global/efdace30-a873-49c7-aaa9-4fa31679ee0c/thumbnails%2F%E5%9B%BE%E6%A0%8701.jpg?1692046715299">
<span class="debug title">WS01の主页</span>
<span class="debug beta">beta</span>
</a>
</header>
</div>
</div>
<div class="add-remove-controls">
<button class="round-btn" onclick="showAddDialog()">+</button>
<button class="round-btn" onclick="toggleRemoveMode()">-</button>
</div>
<div class="sites1"> <!-- 00 -->
<div id="sections-container">
<!-- 分类将在这里动态生成 -->
</div>
<button id="theme-toggle" onclick="toggleTheme()">◑</button>
<div id="dialog-overlay">
<div id="dialog-box">
<label for="name-input">名称</label>
<input type="text" id="name-input">
<label for="url-input">地址</label>
<input type="text" id="url-input">
<label for="category-select">选择分类</label>
<select id="category-select">
<!-- 分类选项将在这里动态生成 -->
</select>
<button onclick="addLink()">确定</button>
<button onclick="hideAddDialog()">取消</button>
</div>
</div>
<div class="copyright">
<!-- 请不要删除 -->
<br />
<!-- body 页脚 -->
<div class="footer">
<input type="password" id="admin-password" placeholder="输入密码">
<button id="admin-mode-btn" onclick="toggleAdminMode()">进入管理模式</button>
<p> <a href="https://github.com/hmhm2022/Card-Tab" target="_blank">GitHub</a> 项目
<!-- 开站时间开始 -->
<span id="timeDate">载入天数...</span><span id="times">载入时分秒...</span> <script language="javascript">
var now = new Date();
function createtime(){
var grt= new Date("09/05/2024 00:00:00");/*---这里是网站的启用时间--*/
now.setTime(now.getTime()+250);
days = (now - grt ) / 1000 / 60 / 60 / 24;
dnum = Math.floor(days);
hours = (now - grt ) / 1000 / 60 / 60 - (24 * dnum);
hnum = Math.floor(hours);
if(String(hnum).length ==1 ){hnum = "0" + hnum;}
minutes = (now - grt ) / 1000 /60 - (24 * 60 * dnum) - (60 * hnum);
mnum = Math.floor(minutes);
if(String(mnum).length ==1 ){mnum = "0" + mnum;}
seconds = (now - grt ) / 1000 - (24 * 60 * 60 * dnum) - (60 * 60 * hnum) - (60 * mnum);
snum = Math.round(seconds);
if(String(snum).length ==1 ){snum = "0" + snum;}
document.getElementById("timeDate").innerHTML = "稳定运行"+dnum+"天";
document.getElementById("times").innerHTML = hnum + "小时" + mnum + "分" + snum + "秒";
}
setInterval("createtime()",250);
</script>
<!-- 开站时间结束 -->
</div>
<script>
let isAdmin = false;
let removeMode = false;
let isDarkTheme = false;
let links = [];
const categories = {
"常·用": [], // **编辑自己的网站分类**
"工·具": [],
"影·音": [],
"N·B·A": [],
"论·坛": [],
"主·页": [],
"玩·具": [],
"v·p·s": [],
"下·载": [],
"商·城": [],
"搜·译": [],
"学·习": [],
"其·它": []
};
async function loadLinks() {
const response = await fetch('/api/getLinks?userId=testUser');
links = await response.json();
Object.keys(categories).forEach(key => {
categories[key] = [];
});
links.forEach(link => {
if (categories[link.category]) {
categories[link.category].push(link);
}
});
loadSections();
updateCategorySelect();
// applyTheme();
}
function loadSections() {
const container = document.getElementById('sections-container');
container.innerHTML = '';
Object.keys(categories).forEach(category => {
const section = document.createElement('div');
section.className = 'section';
const title = document.createElement('div');
title.className = 'section-title';
title.textContent = category;
const cardContainer = document.createElement('div');
cardContainer.className = 'card-container';
cardContainer.id = category;
section.appendChild(title);
section.appendChild(cardContainer);
categories[category].forEach(link => {
createCard(link, cardContainer);
});
container.appendChild(section);
});
}
function createCard(link, container) {
const card = document.createElement('div');
card.className = 'card';
card.setAttribute('draggable', isAdmin);
const cardTop = document.createElement('div');
cardTop.className = 'card-top';
const icon = document.createElement('img');
icon.className = 'card-icon';
// icon.src = 'https://www.google.com/s2/favicons?domain=' + link.url;
icon.src = 'https://favicon.zhusl.com/ico?url=' + link.url;
icon.alt = 'Website Icon';
const title = document.createElement('div');
title.className = 'card-title';
title.textContent = link.name;
cardTop.appendChild(icon);
cardTop.appendChild(title);
const url = document.createElement('div');
url.className = 'card-url';
url.textContent = link.url;
card.appendChild(cardTop);
card.appendChild(url);
// URL 检查和修正
function correctUrl(url) {
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
} else {
return 'http://' + url;
}
}
let correctedUrl = correctUrl(link.url);
if (!isAdmin) {
card.addEventListener('click', () => {
window.open(correctedUrl, '_blank');
});
}
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '–';
deleteBtn.className = 'delete-btn';
deleteBtn.onclick = function (event) {
event.stopPropagation();
removeCard(card);
};
card.appendChild(deleteBtn);
if (isDarkTheme) {
card.style.backgroundColor = '#1e1e1e';
card.style.color = '#ffffff';
card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)';
} else {
card.style.backgroundColor = '#d4d4d4';
card.style.color = '#333';
card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)';
}
card.addEventListener('dragstart', dragStart);
card.addEventListener('dragover', dragOver);
card.addEventListener('dragend', dragEnd);
card.addEventListener('drop', drop);
if (isAdmin && removeMode) {
deleteBtn.style.display = 'block';
}
container.appendChild(card);
}
function updateCategorySelect() {
const categorySelect = document.getElementById('category-select');
categorySelect.innerHTML = '';
Object.keys(categories).forEach(category => {
const option = document.createElement('option');
option.value = category;
option.textContent = category;
categorySelect.appendChild(option);
});
}
async function saveLinks() {
let links = [];
for (const category in categories) {
links = links.concat(categories[category]);
}
await fetch('/api/saveOrder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: 'testUser', links }),
});
}
function addLink() {
const name = document.getElementById('name-input').value;
const url = document.getElementById('url-input').value;
const category = document.getElementById('category-select').value;
if (name && url && category) {
const newLink = { name, url, category };
if (!categories[category]) {
categories[category] = [];
}
categories[category].push(newLink);
const container = document.getElementById(category);
createCard(newLink, container);
saveLinks();
document.getElementById('name-input').value = '';
document.getElementById('url-input').value = '';
hideAddDialog();
}
}
function removeCard(card) {
const url = card.querySelector('.card-url').textContent;
let category;
for (const key in categories) {
const index = categories[key].findIndex(link => link.url === url);
if (index !== -1) {
categories[key].splice(index, 1);
category = key;
break;
}
}
card.remove();
saveLinks();
}
let draggedCard = null;
function dragStart(event) {
if (!isAdmin) return;
draggedCard = event.target;
draggedCard.classList.add('dragging');
event.dataTransfer.effectAllowed = "move";
}
function dragOver(event) {
if (!isAdmin) return;
event.preventDefault();
const target = event.target.closest('.card');
if (target && target !== draggedCard) {
const container = target.parentElement;
const mousePositionX = event.clientX;
const targetRect = target.getBoundingClientRect();
if (mousePositionX < targetRect.left + targetRect.width / 2) {
container.insertBefore(draggedCard, target);
} else {
container.insertBefore(draggedCard, target.nextSibling);
}
}
}
function drop(event) {
if (!isAdmin) return;
event.preventDefault();
draggedCard.classList.remove('dragging');
draggedCard = null;
saveCardOrder();
}
// function dragEnd(event) {
// draggedCard.classList.remove('dragging');
function dragEnd(event) {
if (draggedCard) {
draggedCard.classList.remove('dragging');
}
}
async function saveCardOrder() {
if (!isAdmin) return;
const containers = document.querySelectorAll('.card-container');
let newLinks = [];
containers.forEach(container => {
const category = container.id;
categories[category] = [];
[...container.children].forEach(card => {
const url = card.querySelector('.card-url').textContent;
const name = card.querySelector('.card-title').textContent;
const link = { name, url, category };
categories[category].push(link);
newLinks.push(link);
});
});
links = newLinks;
await fetch('/api/saveOrder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: 'testUser', links: newLinks }),
});
}
function toggleAdminMode() {
const passwordInput = document.getElementById('admin-password');
const adminBtn = document.getElementById('admin-mode-btn');
const addRemoveControls = document.querySelector('.add-remove-controls');
if (!isAdmin) {
verifyPassword(passwordInput.value)
.then(isValid => {
if (isValid) {
isAdmin = true;
adminBtn.textContent = "退出管理模式";
alert('已进入管理模式');
addRemoveControls.style.display = 'block';
reloadCardsAsAdmin();
} else {
alert('密码错误');
}
});
} else {
isAdmin = false;
removeMode = false;
adminBtn.textContent = "进入管理模式";
alert('已退出管理模式');
addRemoveControls.style.display = 'none';
const deleteButtons = document.querySelectorAll('.delete-btn');
deleteButtons.forEach(btn => btn.style.display = 'none');
reloadCardsAsAdmin();
}
passwordInput.value = '';
}
function reloadCardsAsAdmin() {
document.querySelectorAll('.card-container').forEach(container => {
container.innerHTML = '';
});
loadLinks().then(() => {
if (isDarkTheme) {
applyDarkTheme();
}
});
}
function applyDarkTheme() {
document.body.style.backgroundColor = '#121212';
document.body.style.color = '#ffffff';
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
card.style.backgroundColor = '#1e1e1e';
card.style.color = '#ffffff';
card.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.5)';
});
}
function showAddDialog() {
document.getElementById('dialog-overlay').style.display = 'flex';
}
function hideAddDialog() {
document.getElementById('dialog-overlay').style.display = 'none';
}
function toggleRemoveMode() {
removeMode = !removeMode;
const deleteButtons = document.querySelectorAll('.delete-btn');
deleteButtons.forEach(btn => {
btn.style.display = removeMode ? 'block' : 'none';
});
}
function toggleTheme() {
isDarkTheme = !isDarkTheme;
// 设置暗色主题和亮色主题的背景色
document.body.style.backgroundColor = isDarkTheme ? '#696969' : '#FFFFFF';
// 设置暗色主题和亮色主题的文本颜色
document.body.style.color = isDarkTheme ? '#ffffff' : '#333';
const cards = document.querySelectorAll('.card');
cards.forEach(card => {
// 卡片背景和文本颜色设置
card.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#d4d4d4';
card.style.color = isDarkTheme ? '#ffffff' : '#333';
// 卡片阴影的设置,增强暗色主题的阴影
card.style.boxShadow = isDarkTheme
? '0 4px 8px rgba(0, 0, 0, 0.5)'
: '0 4px 8px rgba(0, 0, 0, 0.1)';
});
const dialogBox = document.getElementById('dialog-box');
// 对话框背景和文本颜色设置
dialogBox.style.backgroundColor = isDarkTheme ? '#1e1e1e' : '#ffffff';
dialogBox.style.color = isDarkTheme ? '#ffffff' : '#333';
const inputs = dialogBox.querySelectorAll('input, select');
inputs.forEach(input => {
// 输入框背景和文本颜色设置
input.style.backgroundColor = isDarkTheme ? '#333333' : '#ffffff';
input.style.color = isDarkTheme ? '#ffffff' : '#333';
});
}
async function verifyPassword(inputPassword) {
const response = await fetch('/api/verifyPassword', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: inputPassword }),
});
const result = await response.json();
return result.valid;
}
loadLinks();
</script>
</body>
</html>
`;
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === '/') {
return new Response(HTML_CONTENT, {
headers: { 'Content-Type': 'text/html' }
});
}
if (url.pathname === '/api/getLinks') {
const userId = url.searchParams.get('userId');
const links = await env.CARD_ORDER.get(userId);
return new Response(links || JSON.stringify([]), { status: 200 });
}
if (url.pathname === '/api/saveOrder' && request.method === 'POST') {
const { userId, links } = await request.json();
await env.CARD_ORDER.put(userId, JSON.stringify(links));
return new Response(JSON.stringify({ success: true }), { status: 200 });
}
if (url.pathname === '/api/verifyPassword' && request.method === 'POST') {
const { password } = await request.json();
const isValid = password === env.ADMIN_PASSWORD; // 从环境变量中获取密码
return new Response(JSON.stringify({ valid: isValid }), {
status: isValid ? 200 : 403,
headers: { 'Content-Type': 'application/json' },
});
}
return new Response('Not Found', { status: 404 });
}
};
二、添加变量
1、先建立一个kv空间,名字为: CARD_ORDER ,再在 变量-KV 命名空间绑定 刚才建立的kv空间。
2、变量-环境变量 中添加 ADMIN_PASSWORD ,值是你的后台管理员密码
3、有域名的可邦定域名,完成。
评论 (0)