工具完成
This commit is contained in:
361
src/components/tools/ChromeBookmarkRecovery.vue
Normal file
361
src/components/tools/ChromeBookmarkRecovery.vue
Normal 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>
|
Reference in New Issue
Block a user