工具完成
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing

This commit is contained in:
2025-06-28 22:38:49 +08:00
parent 2c668fedd0
commit 8400dbfab9
60 changed files with 23197 additions and 144 deletions

View File

@ -0,0 +1,361 @@
<template>
<div class="chrome-bookmark-recovery">
<ToolHeader
:title="t('tools.chrome_bookmark_recovery.title')"
:description="t('tools.chrome_bookmark_recovery.description')"
/>
<!-- Windows操作说明 -->
<div class="instruction-content mb-8">
<h3 class="text-lg font-semibold mb-4 text-gray-900 dark:text-gray-100">
{{ t('tools.chrome_bookmark_recovery.instructions.windows.title') }}
</h3>
<ol class="space-y-2 text-gray-700 dark:text-gray-300">
<li
v-for="(step, index) in getInstructionSteps('windows')"
:key="index"
class="flex items-start"
>
<span class="flex-shrink-0 w-6 h-6 bg-blue-500 text-white text-sm rounded-full flex items-center justify-center mr-3 mt-0.5">
{{ index + 1 }}
</span>
<span class="leading-relaxed">{{ step }}</span>
</li>
</ol>
</div>
<!-- 文件上传区域 -->
<div class="upload-section">
<div
ref="dropZone"
@drop="handleDrop"
@dragover="handleDragOver"
@dragleave="handleDragLeave"
:class="[
'border-2 border-dashed rounded-lg p-8 text-center transition-colors',
isDragOver
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500'
]"
>
<div class="space-y-4">
<div class="mx-auto w-12 h-12 text-gray-400">
<svg fill="currentColor" viewBox="0 0 24 24">
<path d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z"/>
</svg>
</div>
<div>
<label class="cursor-pointer">
<input
type="file"
multiple
accept=".bak"
@change="handleFileSelect"
class="hidden"
/>
<span class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 font-medium">
选择 Bookmarks.bak 文件
</span>
</label>
<span class="text-gray-500 dark:text-gray-400"> 或拖拽文件到此处</span>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400">
仅支持 .bak 格式的 Chrome 书签备份文件
</p>
</div>
</div>
<!-- 结果显示 -->
<div v-if="results.length > 0" class="mt-6">
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
<h4 class="font-medium mb-4 text-gray-900 dark:text-gray-100">
处理结果
</h4>
<ul class="space-y-3">
<li
v-for="result in results"
:key="result.id"
class="flex items-center justify-between p-3 bg-white dark:bg-gray-700 rounded border"
>
<div class="flex-1">
<div class="font-medium text-gray-900 dark:text-gray-100">
{{ result.filename }}
</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
最后修改: {{ result.lastModified }} ({{ result.size }}B)
</div>
<div v-if="result.count > 0" class="text-sm text-green-600 dark:text-green-400">
发现 {{ result.count }} 个书签
</div>
<div v-if="result.error" class="text-sm text-red-600 dark:text-red-400">
文件格式错误或无效请选择正确的 Bookmarks.bak 文件
</div>
</div>
<div class="ml-4">
<button
v-if="!result.error"
@click="downloadBookmark(result)"
:disabled="result.downloaded"
class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:bg-gray-400 disabled:cursor-not-allowed text-sm"
>
{{ result.downloaded ? '已下载' : '下载 HTML' }}
</button>
</div>
</li>
</ul>
</div>
</div>
<!-- 加载状态 -->
<div v-if="isLoading" class="mt-6 text-center">
<div class="text-gray-500 dark:text-gray-400">正在处理文件...</div>
</div>
</div>
<!-- 使用说明 -->
<div class="usage-info mt-12">
<h3 class="text-xl font-semibold mb-6 text-gray-900 dark:text-gray-100">
使用说明
</h3>
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-700 rounded-lg p-4">
<div class="flex items-start space-x-3">
<svg class="w-5 h-5 text-blue-500 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
</svg>
<div class="text-sm text-blue-800 dark:text-blue-200">
<p class="font-medium mb-2">导入恢复的书签</p>
<ol class="list-decimal list-inside space-y-1 ml-4">
<li>下载转换后的 HTML 文件</li>
<li>打开 Chrome 浏览器进入 书签 > 书签管理器</li>
<li>点击右上角菜单三个点选择"导入书签"</li>
<li>选择刚才下载的 HTML 文件</li>
<li>书签将被导入到 Chrome </li>
</ol>
</div>
</div>
</div>
</div>
<!-- 免责声明 -->
<div class="disclaimer mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
<p class="text-sm text-gray-500 dark:text-gray-400 italic">
免责声明这不是Google的官方产品请在使用前备份重要数据
</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useLanguage } from '@/composables/useLanguage'
import ToolHeader from '@/components/ToolHeader.vue'
// 语言设置
const { t } = useLanguage()
// 文件上传相关
const dropZone = ref<HTMLElement>()
const isDragOver = ref(false)
const isLoading = ref(false)
const results = ref<Array<{
id: string
filename: string
lastModified: string
size: number
count: number
html: string
error: boolean
downloaded: boolean
}>>([])
// 获取指令步骤
const getInstructionSteps = (tab: string) => {
return t(`tools.chrome_bookmark_recovery.instructions.${tab}.steps`)
}
// Chrome时间转换函数
const chromeTime2TimeT = (time: number): number => {
return Math.floor((time - 11644473600000000) / 1000000)
}
// 书签解析类
class Bookmark {
tree: any
html: string
count: number
first: boolean
constructor(raw: string) {
this.tree = JSON.parse(raw)
this.html = ''
this.count = 0
this.first = true
}
walk = (node: any): void => {
if (node.type === 'folder') {
this.html += `<DT><H3 ADD_DATE="${chromeTime2TimeT(node.date_added)}" LAST_MODIFIED="${chromeTime2TimeT(node.date_modified)}"`
if (this.first) {
this.html += ' PERSONAL_TOOLBAR_FOLDER="true"'
this.first = false
}
this.html += `>${node.name}</H3>\n`
this.html += '<DL><p>\n'
node.children.forEach(this.walk)
this.html += '</DL><p>\n'
} else {
this.html += `<DT><A HREF="${node.url}" ADD_DATE="${chromeTime2TimeT(node.date_added)}">${node.name}</A>\n`
this.count++
}
}
parse = (): void => {
this.html = '<!DOCTYPE NETSCAPE-Bookmark-file-1><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"><TITLE>Bookmarks</TITLE><H1>Bookmarks</H1>\n'
this.html += '<DL><p>\n'
const roots = this.tree.roots
this.walk(roots.bookmark_bar)
if (roots.other.children.length > 0) {
this.walk(roots.other)
}
if (roots.synced.children.length > 0) {
this.walk(roots.synced)
}
this.html += '<style>dt, dl { padding-left: 12px; }</style>\n'
}
}
// 验证文件格式
const isValidBookmarkFile = (filename: string): boolean => {
return filename.toLowerCase().endsWith('.bak') &&
(filename.toLowerCase().includes('bookmark') || filename.toLowerCase() === 'bookmarks.bak')
}
// 处理文件读取
const readFile = (file: File): Promise<any> => {
return new Promise((resolve) => {
const reader = new FileReader()
const result = {
id: Math.random().toString(36).substr(2, 9),
filename: file.name,
lastModified: file.lastModified ? new Date(file.lastModified).toLocaleDateString() : 'n/a',
size: file.size,
count: 0,
html: '',
error: false,
downloaded: false
}
// 验证文件格式
if (!isValidBookmarkFile(file.name)) {
result.error = true
resolve(result)
return
}
reader.onloadend = () => {
try {
const bookmark = new Bookmark(reader.result as string)
bookmark.parse()
result.count = bookmark.count
result.html = bookmark.html
} catch (e) {
result.error = true
}
resolve(result)
}
reader.readAsText(file)
})
}
// 处理文件选择
const handleFileSelect = async (event: Event) => {
const target = event.target as HTMLInputElement
if (target.files) {
await processFiles(Array.from(target.files))
}
}
// 处理文件拖拽
const handleDrop = async (event: DragEvent) => {
event.preventDefault()
event.stopPropagation()
isDragOver.value = false
if (event.dataTransfer?.files) {
await processFiles(Array.from(event.dataTransfer.files))
}
}
const handleDragOver = (event: DragEvent) => {
event.preventDefault()
event.stopPropagation()
isDragOver.value = true
}
const handleDragLeave = (event: DragEvent) => {
event.preventDefault()
event.stopPropagation()
isDragOver.value = false
}
// 处理文件列表
const processFiles = async (files: File[]) => {
isLoading.value = true
results.value = []
try {
for (const file of files) {
const result = await readFile(file)
results.value.push(result)
}
} finally {
isLoading.value = false
}
}
// 下载书签
const downloadBookmark = (result: any) => {
const blob = new Blob([result.html], { type: 'text/html' })
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'chrome_bookmarks_backup.html'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
result.downloaded = true
}
</script>
<style scoped>
.chrome-bookmark-recovery {
max-width: 4xl;
margin: 0 auto;
padding: 1rem;
}
.instruction-content {
min-height: 200px;
}
.upload-section {
margin: 2rem 0;
}
.usage-info {
border-top: 1px solid #e5e7eb;
padding-top: 2rem;
}
@media (max-width: 768px) {
.chrome-bookmark-recovery {
padding: 0.5rem;
}
}
</style>