console
<!DOCTYPE html>
<html>
<head>
<title>全功能导航站</title>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background: #f5f5f5;
}
.directory {
position: fixed;
left: 0;
top: 0;
width: 200px;
height: 100vh;
padding: 20px;
background: #fff;
box-shadow: 2px 0 5px rgba(0,0,0,0.1);
overflow-y: auto;
z-index: 100;
}
.directory-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.add-category-btn {
padding: 5px 10px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
#categoryList {
list-style: none;
}
#categoryList li {
display: flex;
align-items: center;
justify-content: space-between;
margin: 8px 0;
padding: 6px;
border-radius: 4px;
transition: background 0.2s;
}
#categoryList li:hover {
background: #f8f9fa;
}
.category-name {
cursor: pointer;
flex-grow: 1;
margin-right: 8px;
overflow: hidden;
text-overflow: ellipsis;
}
.edit-category-btn {
opacity: 0;
padding: 2px 6px;
background: none;
border: none;
color: #666;
cursor: pointer;
transition: opacity 0.2s;
}
#categoryList li:hover .edit-category-btn {
opacity: 1;
}
.content {
margin-left: 220px;
padding: 30px;
min-height: 100vh;
}
.category-row {
margin-bottom: 30px;
}
.category-header {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.category-header h2 {
margin-right: 15px;
padding-left: 10px;
border-left: 4px solid #007bff;
font-size: 18px;
color: #333;
}
.add-bookmark-btn {
padding: 5px 10px;
background: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.website-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 15px;
}
.website-card {
width: 260px;
height: 120px;
background: white;
padding: 15px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
display: flex;
align-items: center;
position: relative;
transition: transform 0.2s, box-shadow 0.2s;
cursor: pointer;
}
.website-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 12px rgba(0,0,0,0.12);
}
.website-card a {
display: flex;
align-items: center;
color: #333;
text-decoration: none;
width: 100%;
height: 100%;
}
.website-card img {
width: 40px;
height: 40px;
margin-right: 15px;
border-radius: 8px;
object-fit: cover;
border: 1px solid #eee;
background: #fff;
padding: 3px;
}
.website-card span {
font-size: 14px;
max-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.context-menu {
display: none;
position: fixed;
background: white;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
border-radius: 6px;
z-index: 1000;
}
.menu-item {
padding: 8px 16px;
cursor: pointer;
transition: background 0.2s;
}
.menu-item:hover {
background: #f8f9fa;
}
.toolbar {
position: fixed;
top: 20px;
right: 20px;
display: flex;
gap: 10px;
z-index: 100;
}
.toolbar button {
padding: 8px 12px;
background: #17a2b8;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.category-selector {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 15px rgba(0,0,0,0.2);
z-index: 1001;
}
.website-card.dragging {
opacity: 0.5;
transform: scale(0.95);
transition: all 0.2s ease;
}
.website-card.ghost {
background: #f8f9fa;
border: 2px dashed #007bff !important;
opacity: 0.8;
}
#categoryList li.dragging {
background: #f8f9fa;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#categoryList li.ghost {
opacity: 0.5;
border-left: 3px solid #007bff;
}
</style>
</head>
<body>
<div class="directory">
<div class="directory-header">
<h2>网站分类</h2>
<button class="add-category-btn" onclick="addCategory()">+ 类目</button>
</div>
<ul id="categoryList"></ul>
</div>
<div class="content" id="content"></div>
<div class="toolbar">
<button onclick="exportData()">导出数据</button>
<button onclick="document.getElementById('fileInput').click()">导入数据</button>
<input type="file" id="fileInput" hidden accept=".json" onchange="importData(event)">
</div>
<div id="contextMenu" class="context-menu">
<div class="menu-item" onclick="handleMenu('edit')">修改图标</div>
<div class="menu-item" onclick="handleMenu('move')">调整分类</div>
<div class="menu-item" onclick="handleMenu('delete')">删除书签</div>
</div>
<div id="categorySelector" class="category-selector">
<h3>移动到分类:</h3>
<div id="targetCategoryList"></div>
<button onclick="closeCategorySelector()" style="margin-top:10px;">取消</button>
</div>
<script>
let data = JSON.parse(localStorage.getItem('navData')) || {
categories: [
{
id: 'default',
name: '默认分类',
bookmarks: []
}
]
};
let currentContext = null;
let sortableInstances = [];
let categorySortable = null;
function render() {
renderDirectory();
renderContent();
initSortable();
}
function renderDirectory() {
const categoryList = document.getElementById('categoryList');
categoryList.innerHTML = data.categories.map(cat => `
<li data-category-id="${cat.id}">
<span class="category-name">${cat.name}</span>
<button class="edit-category-btn" onclick="editCategoryName('${cat.id}')">✎</button>
</li>
`).join('');
}
function renderContent() {
const content = document.getElementById('content');
content.innerHTML = data.categories.map(cat => `
<div class="category-row" id="${cat.id}">
<div class="category-header">
<h2>${cat.name}</h2>
<button class="add-bookmark-btn" onclick="addBookmark('${cat.id}')">+ 书签</button>
</div>
<div class="website-grid">
${cat.bookmarks.map(bookmark => `
<div class="website-card"
data-category="${cat.id}"
data-url="${bookmark.url}"
oncontextmenu="showContextMenu(event, '${cat.id}', '${bookmark.url}')">
<a href="${bookmark.url}" target="_blank">
<img src="${getIconUrl(bookmark)}"
onerror="this.onerror=null;this.src='${generateLetterIcon(bookmark.name)}'">
<span>${bookmark.name}</span>
</a>
</div>
`).join('')}
</div>
</div>
`).join('');
localStorage.setItem('navData', JSON.stringify(data));
}
function initSortable() {
if(categorySortable) categorySortable.destroy();
sortableInstances.forEach(instance => instance.destroy());
sortableInstances = [];
categorySortable = new Sortable(document.getElementById('categoryList'), {
animation: 150,
handle: '.category-name',
ghostClass: 'ghost',
chosenClass: 'dragging',
delay: 300,
delayOnTouchOnly: true,
onEnd: (evt) => {
const newOrder = Array.from(evt.from.children).map(li =>
data.categories.find(c => c.id === li.dataset.categoryId)
);
data.categories = newOrder.filter(Boolean);
renderContent();
}
});
document.querySelectorAll('.website-grid').forEach(container => {
const sortable = new Sortable(container, {
animation: 150,
ghostClass: 'ghost',
chosenClass: 'dragging',
delay: 300,
delayOnTouchOnly: true,
onStart: () => {
document.getElementById('contextMenu').style.display = 'none';
},
onEnd: (evt) => {
const categoryId = evt.from.closest('.category-row').id;
const category = data.categories.find(c => c.id === categoryId);
const newOrder = Array.from(evt.from.children).map(item =>
category.bookmarks.find(b => b.url === item.dataset.url)
).filter(Boolean);
category.bookmarks = newOrder;
localStorage.setItem('navData', JSON.stringify(data));
}
});
sortableInstances.push(sortable);
});
}
function addCategory() {
const name = prompt('请输入新分类名称');
if (name) {
data.categories.push({
id: `category-${Date.now()}`,
name: name,
bookmarks: []
});
render();
}
}
function editCategoryName(categoryId) {
const category = data.categories.find(cat => cat.id === categoryId);
if (!category) return;
const newName = prompt('请输入新分类名称', category.name);
if (newName) {
category.name = newName;
render();
}
}
function addBookmark(categoryId) {
const name = prompt('请输入网站名称');
const url = prompt('请输入网站地址');
if (name && url) {
try {
new URL(url);
const category = data.categories.find(cat => cat.id === categoryId);
if (category) {
category.bookmarks.push({
name,
url,
customIcon: ''
});
render();
}
} catch {
alert('请输入有效的网址!');
}
}
}
function showContextMenu(e, categoryId, url) {
e.preventDefault();
e.stopPropagation();
currentContext = { categoryId, url };
const menu = document.getElementById('contextMenu');
menu.style.display = 'block';
menu.style.left = `${e.pageX}px`;
menu.style.top = `${e.pageY}px`;
}
document.addEventListener('click', () => {
document.getElementById('contextMenu').style.display = 'none';
});
function handleMenu(action) {
switch(action) {
case 'edit':
editIcon();
break;
case 'move':
showMoveSelector();
break;
case 'delete':
deleteBookmark();
break;
}
}
function deleteBookmark() {
if (!currentContext) return;
if (confirm('确定要删除这个书签吗?')) {
const category = data.categories.find(cat => cat.id === currentContext.categoryId);
if (category) {
category.bookmarks = category.bookmarks.filter(b => b.url !== currentContext.url);
render();
}
}
}
function getIconUrl(bookmark) {
return bookmark.customIcon ||
`https://www.google.com/s2/favicons?domain=${new URL(bookmark.url).hostname}&sz=64`;
}
function editIcon() {
const bookmark = getCurrentBookmark();
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = (e) => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = () => {
bookmark.customIcon = reader.result;
render();
};
reader.readAsDataURL(file);
};
input.click();
}
function showMoveSelector() {
const selector = document.getElementById('categorySelector');
selector.style.display = 'block';
document.getElementById('targetCategoryList').innerHTML = data.categories
.filter(cat => cat.id !== currentContext.categoryId)
.map(cat => `
<div class="menu-item" onclick="moveBookmark('${cat.id}')">${cat.name}</div>
`).join('');
}
function moveBookmark(targetCategoryId) {
const sourceCat = data.categories.find(c => c.id === currentContext.categoryId);
const targetCat = data.categories.find(c => c.id === targetCategoryId);
const bookmark = sourceCat.bookmarks.find(b => b.url === currentContext.url);
sourceCat.bookmarks = sourceCat.bookmarks.filter(b => b.url !== currentContext.url);
targetCat.bookmarks.push(bookmark);
render();
closeCategorySelector();
}
function exportData() {
const json = JSON.stringify(data);
const blob = new Blob([json], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'nav-data.json';
a.click();
URL.revokeObjectURL(url);
}
function importData(event) {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = (e) => {
try {
data = JSON.parse(e.target.result);
localStorage.setItem('navData', JSON.stringify(data));
render();
} catch {
alert('无效的数据文件!');
}
};
reader.readAsText(file);
}
function scrollToCategory(categoryId) {
const element = document.getElementById(categoryId);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}
function getCurrentBookmark() {
return data.categories
.find(c => c.id === currentContext.categoryId)
.bookmarks.find(b => b.url === currentContext.url);
}
function generateLetterIcon(name) {
const letter = (name || '?').charAt(0).toUpperCase();
const hue = stringToHash(name) % 360;
return `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'>
<rect width='100%' height='100%' fill='hsl(${hue},70%,85%)' rx='15'/>
<text x='50%' y='55%' font-size='50' dominant-baseline='middle' text-anchor='middle'
fill='hsl(${hue},60%,30%)' font-family='Arial'>${letter}</text>
</svg>`;
}
function stringToHash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
return Math.abs(hash);
}
function closeCategorySelector() {
document.getElementById('categorySelector').style.display = 'none';
}
render();
</script>
</body>
</html>