361 lines
11 KiB
Vue
361 lines
11 KiB
Vue
<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> |