Files
utils/src/components/tools/ChromeBookmarkRecovery.vue
zguiy 8400dbfab9
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
工具完成
2025-06-28 22:38:49 +08:00

361 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>