feat(demo): 添加书签功能支持保存和跳转页面

添加书签侧边栏组件,支持以下功能:
1. 自动生成书签名称并保存当前可见页码
2. 书签列表展示、编辑和删除功能
3. 点击书签跳转到指定页面
4. 使用localStorage持久化存储书签数据
This commit is contained in:
hanshiyang 2025-11-28 16:53:44 +08:00
parent 244a86a885
commit 27c85de1c4

169
demo.html
View File

@ -9,6 +9,19 @@
.panel { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 12px; }
.panel input[type="text"] { width: 420px; padding: 6px 8px; font-size: 12px; }
.panel button { padding: 6px 12px; font-size: 12px; cursor: pointer; }
.layout { display: flex; gap: 12px; align-items: flex-start; }
.sidebar { width: 280px; border: 1px solid #e2e6ed; border-radius: 8px; padding: 12px; background: #fafafa; }
.sidebar h4 { margin: 0 0 8px; font-weight: 600; font-size: 14px; }
.bookmark-form { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
.bookmark-form input { flex: 1; min-width: 0; padding: 6px 8px; font-size: 12px; }
.bookmark-form input[type="number"] { width: 80px; }
.bookmark-form button { padding: 6px 12px; font-size: 12px; cursor: pointer; }
.bookmark-list { list-style: none; padding: 0; margin: 0; }
.bookmark-item { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px; border-bottom: 1px solid #eee; }
.bookmark-item span { font-size: 12px; color: #111827; }
.bookmark-actions { display: flex; gap: 6px; }
.bookmark-actions button { padding: 4px 8px; font-size: 12px; cursor: pointer; }
.editorWrap { flex: 1; }
iframe { width: 100%; height: 80vh; border: 1px solid #e2e6ed; }
small { color: #6b7280; }
</style>
@ -24,13 +37,27 @@
<button id="loadBtn">设置 iframe 地址</button>
<small>示例vocd.html?filePath=URL&mode=edit 或 vocd.html?filePath=URL</small>
</div>
<iframe id="editorFrame" src="./vocd.html" referrerpolicy="no-referrer"></iframe>
<div class="layout">
<aside class="sidebar">
<h4>书签</h4>
<div class="bookmark-form">
<button id="addBookmarkBtn">添加书签</button>
<small>自动命名为书签N+1页码取当前可见页</small>
</div>
<ul id="bookmarkList" class="bookmark-list"></ul>
</aside>
<div class="editorWrap">
<iframe id="editorFrame" src="./vocd.html" referrerpolicy="no-referrer"></iframe>
</div>
</div>
<script>
const editorFrame = document.getElementById('editorFrame')
const loadBtn = document.getElementById('loadBtn')
const urlInput = document.getElementById('urlInput')
const editModeToggle = document.getElementById('editModeToggle')
const addBookmarkBtn = document.getElementById('addBookmarkBtn')
const bookmarkList = document.getElementById('bookmarkList')
function buildSrc(filePath, isEdit) {
const base = './vocd.html'
@ -47,6 +74,7 @@
const src = buildSrc(defaultUrl, editModeToggle.checked)
editorFrame.src = src
console.log('[demo] 自动设置 iframe src:', src)
renderBookmarks()
})
// 切换编辑模式时,刷新 iframe 地址,保留当前 filePath
@ -58,6 +86,7 @@
const src = buildSrc(currentFilePath, editModeToggle.checked)
editorFrame.src = src
console.log('[demo] 编辑模式切换,刷新 iframe src:', src)
renderBookmarks()
})
loadBtn.onclick = () => {
@ -67,7 +96,145 @@
const src = buildSrc(targetUrl, editModeToggle.checked)
editorFrame.src = src
console.log('[demo] 设置 iframe src:', src)
renderBookmarks()
}
function getCurrentFilePath() {
const currentAbs = editorFrame.src || './vocd.html'
const currentUrl = new URL(currentAbs, window.location.href)
return currentUrl.searchParams.get('filePath') || '/test.ocd'
}
function getBookmarkKey() {
const fp = getCurrentFilePath()
return `bookmarks:${fp}`
}
function readBookmarks() {
try {
const raw = localStorage.getItem(getBookmarkKey())
return raw ? JSON.parse(raw) : []
} catch {
return []
}
}
function writeBookmarks(list) {
localStorage.setItem(getBookmarkKey(), JSON.stringify(list))
}
function getNextBookmarkName() {
const list = readBookmarks()
let maxN = 0
list.forEach(b => {
const m = /^书签(\d+)$/.exec(String(b.name || ''))
if (m) {
const n = parseInt(m[1], 10)
if (Number.isFinite(n)) maxN = Math.max(maxN, n)
}
})
return `书签${maxN + 1}`
}
function renderBookmarks() {
const list = readBookmarks()
bookmarkList.innerHTML = ''
list.forEach(item => {
const li = document.createElement('li')
li.className = 'bookmark-item'
const label = document.createElement('span')
label.textContent = `${item.name}(第${item.page}页)`
const actions = document.createElement('div')
actions.className = 'bookmark-actions'
const jumpBtn = document.createElement('button')
jumpBtn.textContent = '跳转'
jumpBtn.onclick = () => navigateToPage(item.page)
const editBtn = document.createElement('button')
editBtn.textContent = '编辑'
editBtn.onclick = () => {
const newName = window.prompt('书签名称', item.name) || item.name
const newPageStr = window.prompt('页码(正整数)', String(item.page)) || String(item.page)
const newPage = Math.max(1, parseInt(newPageStr, 10) || item.page)
const list2 = readBookmarks().map(b => (b.id === item.id ? { ...b, name: newName, page: newPage } : b))
writeBookmarks(list2)
renderBookmarks()
}
const delBtn = document.createElement('button')
delBtn.textContent = '删除'
delBtn.onclick = () => {
const list2 = readBookmarks().filter(b => b.id !== item.id)
writeBookmarks(list2)
renderBookmarks()
}
actions.appendChild(jumpBtn)
actions.appendChild(editBtn)
actions.appendChild(delBtn)
li.appendChild(label)
li.appendChild(actions)
bookmarkList.appendChild(li)
})
}
function addBookmark(name, page) {
const list = readBookmarks()
const id = Date.now().toString(36) + Math.random().toString(36).slice(2)
const item = { id, name, page }
writeBookmarks([...list, item])
renderBookmarks()
}
addBookmarkBtn.onclick = () => {
const name = getNextBookmarkName()
const page = getFirstVisiblePageNo()
addBookmark(name, page)
}
function getFirstVisiblePageNo() {
const doc = editorFrame.contentDocument || editorFrame.contentWindow?.document
if (!doc) return 1
const iframeRect = editorFrame.getBoundingClientRect()
const canvases = Array.from(doc.querySelectorAll('canvas[data-index]'))
if (!canvases.length) return 1
const visible = canvases
.map(c => {
const rect = c.getBoundingClientRect()
const overlapY = Math.min(rect.bottom, iframeRect.bottom) - Math.max(rect.top, iframeRect.top)
return { c, rect, overlapY }
})
.filter(x => x.overlapY > 0)
.sort((a, b) => a.rect.top - b.rect.top)
let chosen = visible.length ? visible[0].c : null
if (!chosen) {
chosen = canvases.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top)[0]
}
const idxStr = chosen?.getAttribute('data-index') || '0'
const idx = parseInt(idxStr, 10)
return Number.isFinite(idx) ? idx + 1 : 1
}
function navigateToPage(page) {
const doc = editorFrame.contentDocument || editorFrame.contentWindow?.document
if (!doc) return
const target0 = doc.querySelector(`canvas[data-index="${page - 1}"]`)
const target1 = doc.querySelector(`canvas[data-index="${page}"]`)
const canvas = target0 || target1
if (!canvas) {
alert(`未找到第${page}页`)
return
}
try {
canvas.scrollIntoView({ behavior: 'smooth', block: 'center' })
const evt = new MouseEvent('mousedown', { bubbles: true })
canvas.dispatchEvent(evt)
} catch (e) {
console.warn('跳转页失败', e)
}
}
editorFrame.addEventListener('load', () => {
renderBookmarks()
})
</script>
</body>
</html>