mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-06-18 15:54:08 +08:00
improvement(entry-ui): 优化检测与导入首屏流程
- 重构检测结果页布局,补充版本/账号/数据库统计与更清晰的加载态 - 首页移除直接解密入口,/import 页面隐藏侧边栏,统一首屏使用路径 - 导入页优化预览、错误和重试体验,并补充 SSE 连接清理逻辑
This commit is contained in:
+1
-1
@@ -101,7 +101,7 @@ const showDesktopTitleBar = computed(() => isDesktop.value)
|
||||
|
||||
const showSidebar = computed(() => {
|
||||
const path = String(route.path || '')
|
||||
if (path === '/') return false
|
||||
if (path === '/' || path === '/import') return false
|
||||
if (path === '/decrypt' || path === '/detection-result' || path === '/decrypt-result') return false
|
||||
return !(path === '/wrapped' || path.startsWith('/wrapped/'))
|
||||
})
|
||||
|
||||
+183
-151
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="detection-result-page min-h-screen relative overflow-hidden flex items-center">
|
||||
<div class="detection-result-page min-h-screen relative overflow-hidden">
|
||||
<!-- 网格背景 -->
|
||||
<div class="absolute inset-0 bg-grid-pattern opacity-5 pointer-events-none"></div>
|
||||
|
||||
@@ -9,14 +9,16 @@
|
||||
<div class="absolute -bottom-8 left-40 w-80 h-80 bg-[#91D300] opacity-5 rounded-full blur-3xl pointer-events-none"></div>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<div class="relative z-10 w-full max-w-6xl mx-auto px-4 py-8 animate-fade-in">
|
||||
<div class="relative z-10 w-full max-w-5xl mx-auto px-4 sm:px-5 py-6 sm:py-8 animate-fade-in">
|
||||
<!-- 顶部操作栏 -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-2xl font-bold">
|
||||
<span class="bg-gradient-to-r from-[#07C160] to-[#10AEEF] bg-clip-text text-transparent">检测结果</span>
|
||||
</h2>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 class="text-[22px] font-bold leading-none">
|
||||
<span class="bg-gradient-to-r from-[#07C160] to-[#10AEEF] bg-clip-text text-transparent">检测结果</span>
|
||||
</h2>
|
||||
</div>
|
||||
<NuxtLink to="/"
|
||||
class="inline-flex items-center px-3 py-1.5 text-sm text-[#07C160] hover:text-[#06AD56] font-medium transition-colors">
|
||||
class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs text-[#07C160] hover:text-[#06AD56] hover:bg-white/80 font-medium transition-colors">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
@@ -24,90 +26,167 @@
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- 兜底:唤起原生目录选择器再次检测 -->
|
||||
<div class="bg-white rounded-xl p-4 md:p-5 border border-[#EDEDED] mb-6 flex flex-col md:flex-row md:items-center justify-between gap-4 shadow-sm hover:shadow transition-shadow duration-200">
|
||||
<div>
|
||||
<h3 class="text-sm font-bold text-[#000000e6] flex items-center">
|
||||
未找到想要的账号?
|
||||
<!-- <span class="ml-2 px-2 py-0.5 bg-gray-100 text-gray-500 rounded text-xs font-normal">深度检测兜底</span>-->
|
||||
</h3>
|
||||
<p class="text-xs text-[#7F7F7F] mt-1.5">
|
||||
<span v-if="customPath">当前指定检测路径:<span class="font-mono bg-gray-50 px-1 rounded text-[#000000e6]">{{ customPath }}</span></span>
|
||||
<span v-else>如果自动检测漏了,您可以手动指定微信数据根目录 (通常名为 xwechat_files) 让系统重新扫描。</span>
|
||||
</p>
|
||||
<div class="mt-3">
|
||||
<label for="wechatInstallPath" class="block text-xs font-medium text-[#000000e6]">微信安装目录(可选)</label>
|
||||
<div class="mt-2 flex flex-col lg:flex-row gap-3">
|
||||
<input
|
||||
id="wechatInstallPath"
|
||||
v-model="wechatInstallPath"
|
||||
type="text"
|
||||
placeholder="例如: D:\Program Files\Tencent\WeChat 或 D:\Program Files\Tencent\WeChat\Weixin.exe"
|
||||
class="flex-1 px-3 py-2 bg-white border border-[#EDEDED] rounded-lg font-mono text-xs focus:outline-none focus:ring-2 focus:ring-[#07C160] focus:border-transparent transition-all duration-200"
|
||||
@blur="persistWechatInstallPath"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="pickWechatInstallDirectory"
|
||||
:disabled="isPickingWechatInstallPath"
|
||||
class="shrink-0 px-4 py-2 bg-white border border-[#EDEDED] text-[#000000e6] rounded-lg text-xs font-medium hover:bg-gray-50 disabled:opacity-50 disabled:cursor-wait transition-all duration-200"
|
||||
>
|
||||
{{ isPickingWechatInstallPath ? '选择中...' : '选择微信目录' }}
|
||||
</button>
|
||||
<div
|
||||
v-if="detectionResult && !loading && !detectionResult.error"
|
||||
class="grid grid-cols-1 sm:grid-cols-3 gap-2.5 mb-4"
|
||||
>
|
||||
<div class="bg-white/90 backdrop-blur rounded-xl px-4 py-3 border border-[#EDEDED]">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<p class="text-[11px] tracking-[0.08em] uppercase text-[#7F7F7F]">微信版本</p>
|
||||
<p class="mt-1 text-lg font-semibold text-[#000000e6] truncate">{{ detectionResult.data?.wechat_version || '未知' }}</p>
|
||||
</div>
|
||||
<div class="w-9 h-9 shrink-0 bg-[#07C160]/10 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-[18px] h-[18px] text-[#07C160]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-xs text-[#7F7F7F] mt-2">
|
||||
一键获取数据库密钥会优先使用这里填写的路径。支持安装目录或 Weixin.exe / WeChat.exe 路径。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="handlePickDirectory" :disabled="loading"
|
||||
class="shrink-0 px-5 py-2.5 bg-[#07C160] text-white rounded-xl text-sm font-medium hover:bg-[#06AD56] focus:ring-2 focus:ring-[#07C160] focus:ring-offset-1 disabled:opacity-50 transition-all duration-200 flex items-center justify-center">
|
||||
<svg v-if="!loading" class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
||||
</svg>
|
||||
<svg v-else class="w-4 h-4 mr-2 animate-spin" fill="none" viewBox="0 0 48 48" aria-hidden="true">
|
||||
<circle class="opacity-20" cx="24" cy="24" r="18" stroke="currentColor" stroke-width="6"></circle>
|
||||
<circle
|
||||
cx="24"
|
||||
cy="24"
|
||||
r="18"
|
||||
stroke="currentColor"
|
||||
stroke-width="6"
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray="28 72"
|
||||
pathLength="100"
|
||||
transform="rotate(-90 24 24)"
|
||||
></circle>
|
||||
</svg>
|
||||
{{ loading ? '检测中...' : '手动选择目录检测' }}
|
||||
</button>
|
||||
|
||||
<div class="bg-white/90 backdrop-blur rounded-xl px-4 py-3 border border-[#EDEDED]">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<p class="text-[11px] tracking-[0.08em] uppercase text-[#7F7F7F]">检测账号</p>
|
||||
<p class="mt-1 text-lg font-semibold text-[#000000e6]">{{ detectionResult.data?.total_accounts || 0 }} 个</p>
|
||||
</div>
|
||||
<div class="w-9 h-9 shrink-0 bg-[#10AEEF]/10 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-[18px] h-[18px] text-[#10AEEF]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283-.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white/90 backdrop-blur rounded-xl px-4 py-3 border border-[#EDEDED]">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<p class="text-[11px] tracking-[0.08em] uppercase text-[#7F7F7F]">数据库文件</p>
|
||||
<p class="mt-1 text-lg font-semibold text-[#000000e6]">{{ detectionResult.data?.total_databases || 0 }} 个</p>
|
||||
</div>
|
||||
<div class="w-9 h-9 shrink-0 bg-[#91D300]/10 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-[18px] h-[18px] text-[#91D300]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第一步检测:数据目录与微信安装目录都在这里设置 -->
|
||||
<div v-if="!loading" class="bg-white/90 backdrop-blur rounded-xl p-3.5 md:p-4 border border-[#EDEDED] mb-4 space-y-3">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="text-[13px] font-semibold text-[#000000e6] flex items-center">
|
||||
未找到想要的账号?
|
||||
<!-- <span class="ml-2 px-2 py-0.5 bg-gray-100 text-gray-500 rounded text-xs font-normal">深度检测兜底</span>-->
|
||||
</h3>
|
||||
<p class="text-[11px] text-[#7F7F7F] mt-1">
|
||||
<span v-if="customPath">当前指定检测路径:<span class="font-mono bg-gray-50 px-1 rounded text-[#000000e6]">{{ customPath }}</span></span>
|
||||
<span v-else>如果自动检测漏了,您可以手动指定微信数据根目录 (通常名为 xwechat_files) 让系统重新扫描。</span>
|
||||
</p>
|
||||
</div>
|
||||
<button @click="handlePickDirectory" :disabled="loading"
|
||||
class="shrink-0 px-4 py-2.5 bg-[#07C160] text-white rounded-xl text-xs font-medium hover:bg-[#06AD56] focus:ring-2 focus:ring-[#07C160] focus:ring-offset-1 disabled:opacity-50 transition-all duration-200 flex items-center justify-center">
|
||||
<svg v-if="!loading" class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
||||
</svg>
|
||||
<svg v-else class="w-4 h-4 mr-2 animate-spin" fill="none" viewBox="0 0 48 48" aria-hidden="true">
|
||||
<circle class="opacity-20" cx="24" cy="24" r="18" stroke="currentColor" stroke-width="6"></circle>
|
||||
<circle
|
||||
cx="24"
|
||||
cy="24"
|
||||
r="18"
|
||||
stroke="currentColor"
|
||||
stroke-width="6"
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray="28 72"
|
||||
pathLength="100"
|
||||
transform="rotate(-90 24 24)"
|
||||
></circle>
|
||||
</svg>
|
||||
{{ loading ? '检测中...' : '手动选择目录检测' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="pt-3 border-t border-[#F3F3F3]">
|
||||
<label for="wechatInstallPath" class="block text-[13px] font-medium text-[#000000e6] mb-2">
|
||||
微信安装目录(第一步先填这里)
|
||||
</label>
|
||||
<div class="flex flex-col lg:flex-row gap-3">
|
||||
<input
|
||||
id="wechatInstallPath"
|
||||
v-model="wechatInstallPath"
|
||||
type="text"
|
||||
placeholder="例如: D:\Program Files\Tencent\WeChat 或 D:\Program Files\Tencent\WeChat\Weixin.exe"
|
||||
class="flex-1 px-4 py-2.5 bg-white border border-[#EDEDED] rounded-lg font-mono text-[13px] focus:outline-none focus:ring-2 focus:ring-[#07C160] focus:border-transparent transition-all duration-200"
|
||||
@blur="persistWechatInstallPath"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="pickWechatInstallDirectory"
|
||||
:disabled="isPickingWechatInstallPath"
|
||||
class="shrink-0 px-4 py-2.5 bg-white border border-[#EDEDED] text-[#000000e6] rounded-xl text-xs font-medium hover:bg-gray-50 disabled:opacity-50 disabled:cursor-wait transition-all duration-200"
|
||||
>
|
||||
{{ isPickingWechatInstallPath ? '选择中...' : '选择微信目录' }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-[11px] text-[#7F7F7F] mt-2">
|
||||
一键获取数据库密钥会优先使用这里填写的路径。支持填写安装目录,也支持直接填写 <span class="font-mono">Weixin.exe</span> / <span class="font-mono">WeChat.exe</span> 路径。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<div>
|
||||
<div :class="loading ? 'flex min-h-[52vh] items-center justify-center' : ''">
|
||||
<!-- 检测中状态 -->
|
||||
<div v-if="loading" class="absolute inset-0 bg-white/80 backdrop-blur-sm z-20 rounded-2xl flex flex-col items-center justify-center border border-[#EDEDED]">
|
||||
<svg class="w-16 h-16 animate-spin text-[#07C160]" fill="none" viewBox="0 0 48 48" aria-hidden="true">
|
||||
<circle class="opacity-20" cx="24" cy="24" r="18" stroke="currentColor" stroke-width="6"></circle>
|
||||
<circle
|
||||
cx="24"
|
||||
cy="24"
|
||||
r="18"
|
||||
stroke="currentColor"
|
||||
stroke-width="6"
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray="28 72"
|
||||
pathLength="100"
|
||||
transform="rotate(-90 24 24)"
|
||||
></circle>
|
||||
</svg>
|
||||
<p class="mt-4 text-lg text-[#7F7F7F]">正在检测微信数据...</p>
|
||||
<div v-if="loading" class="w-full max-w-3xl rounded-[24px] border border-[#EDEDED] bg-white/92 px-5 py-6 sm:px-8 sm:py-7">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="relative flex h-12 w-12 items-center justify-center rounded-2xl bg-[#07C160]/10">
|
||||
<span class="absolute inset-0 rounded-2xl border border-[#07C160]/10"></span>
|
||||
<svg class="h-5 w-5 animate-spin text-[#07C160]" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2.5" class="opacity-20"></circle>
|
||||
<path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-wrap items-center justify-center gap-2">
|
||||
<span class="inline-flex items-center rounded-full bg-[#07C160]/10 px-2.5 py-1 text-[11px] font-medium text-[#07C160]">
|
||||
检测中
|
||||
</span>
|
||||
<span class="text-[11px] text-[#7F7F7F]">正在自动读取本机微信环境</span>
|
||||
</div>
|
||||
|
||||
<h3 class="mt-3 text-[20px] font-semibold text-[#000000e6] leading-tight">正在检查账号与数据库文件</h3>
|
||||
<p class="mt-2 max-w-[560px] text-[13px] leading-6 text-[#7F7F7F]">
|
||||
会依次确认微信安装信息、最近登录账号以及可用数据库,通常几秒内完成。
|
||||
</p>
|
||||
|
||||
<div class="mt-5 h-1.5 w-full max-w-[620px] overflow-hidden rounded-full bg-[#F3F4F6]">
|
||||
<div class="h-full w-2/5 rounded-full bg-gradient-to-r from-[#07C160] via-[#34D17A] to-[#8CE0AF] animate-pulse"></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 flex flex-wrap items-center justify-center gap-2.5">
|
||||
<div class="inline-flex items-center gap-2 rounded-full border border-[#EDEDED] bg-[#FAFAFA] px-3 py-2 text-[12px] text-[#000000d9]">
|
||||
<span class="h-2 w-2 rounded-full bg-[#07C160] animate-pulse"></span>
|
||||
<span>安装信息</span>
|
||||
</div>
|
||||
<div class="inline-flex items-center gap-2 rounded-full border border-[#EDEDED] bg-[#FAFAFA] px-3 py-2 text-[12px] text-[#000000d9]">
|
||||
<span class="h-2 w-2 rounded-full bg-[#07C160] animate-pulse"></span>
|
||||
<span>账号匹配</span>
|
||||
</div>
|
||||
<div class="inline-flex items-center gap-2 rounded-full border border-[#EDEDED] bg-[#FAFAFA] px-3 py-2 text-[12px] text-[#000000d9]">
|
||||
<span class="h-2 w-2 rounded-full bg-[#07C160] animate-pulse"></span>
|
||||
<span>数据库汇总</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 检测结果内容 -->
|
||||
|
||||
<!-- detection result content -->
|
||||
<div v-if="detectionResult && !loading">
|
||||
<!-- 错误信息 -->
|
||||
<div v-if="detectionResult.error" class="bg-red-50 rounded-2xl border border-red-100 p-8">
|
||||
<div v-if="detectionResult.error" class="bg-red-50 rounded-2xl border border-red-100 p-6">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-8 h-8 text-red-500 mr-3 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
@@ -120,95 +199,49 @@
|
||||
</div>
|
||||
|
||||
<!-- 成功结果 -->
|
||||
<div v-else class="space-y-4">
|
||||
<!-- 概览卡片 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div class="bg-white rounded-xl p-4 border border-[#EDEDED]">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-[#7F7F7F]">微信版本</p>
|
||||
<p class="text-xl font-bold text-[#000000e6] mt-1">{{ detectionResult.data?.wechat_version || '未知' }}</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-[#07C160]/10 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-[#07C160]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl p-4 border border-[#EDEDED]">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-[#7F7F7F]">检测到的账户</p>
|
||||
<p class="text-xl font-bold text-[#000000e6] mt-1">{{ detectionResult.data?.total_accounts || 0 }} 个</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-[#10AEEF]/10 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-[#10AEEF]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283-.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl p-4 border border-[#EDEDED]">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-[#7F7F7F]">数据库文件</p>
|
||||
<p class="text-xl font-bold text-[#000000e6] mt-1">{{ detectionResult.data?.total_databases || 0 }} 个</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-[#91D300]/10 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-[#91D300]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-3">
|
||||
<!-- 账户列表 -->
|
||||
<div v-if="detectionResult.data?.accounts && detectionResult.data.accounts.length > 0"
|
||||
class="bg-white rounded-2xl border border-[#EDEDED] overflow-hidden shadow-sm">
|
||||
<div class="p-4 border-b border-[#EDEDED] bg-gray-50 flex items-center justify-between">
|
||||
<h3 class="text-base font-bold text-[#000000e6]">可操作的微信账户</h3>
|
||||
<span class="text-xs text-gray-500">点击解密即可提取数据</span>
|
||||
class="bg-white/92 backdrop-blur rounded-2xl border border-[#EDEDED] overflow-hidden">
|
||||
<div class="px-4 py-3 border-b border-[#EDEDED] bg-[#fafafa] flex items-center justify-between">
|
||||
<h3 class="text-[15px] font-semibold text-[#000000e6]">可操作的微信账户</h3>
|
||||
<span class="text-[11px] text-gray-500">点击解密即可提取数据</span>
|
||||
</div>
|
||||
<div class="divide-y divide-[#EDEDED] max-h-96 overflow-y-auto">
|
||||
<div class="divide-y divide-[#EDEDED] max-h-[420px] overflow-y-auto">
|
||||
<div v-for="(account, index) in sortedAccounts" :key="index"
|
||||
:class="['p-5 transition-all duration-200 relative overflow-hidden', isCurrentAccount(account.account_name) ? 'bg-[#07C160]/5 border border-[#07C160]/20' : 'hover:bg-[#F9F9F9]']">
|
||||
:class="['px-4 py-3.5 transition-all duration-200 relative overflow-hidden', isCurrentAccount(account.account_name) ? 'bg-[#07C160]/5 border border-[#07C160]/20' : 'hover:bg-[#F9F9F9]']">
|
||||
|
||||
<div v-if="isCurrentAccount(account.account_name)" class="absolute top-0 right-0 bg-gradient-to-l from-[#07C160]/20 to-transparent px-4 py-1 rounded-bl-xl flex items-center">
|
||||
<div v-if="isCurrentAccount(account.account_name)" class="absolute top-0 right-0 bg-gradient-to-l from-[#07C160]/20 to-transparent px-3 py-1 rounded-bl-xl flex items-center">
|
||||
<span class="text-xs text-[#07C160] font-bold flex items-center">
|
||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
||||
最近登录账户
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between mt-1">
|
||||
<div class="flex items-center justify-between gap-3 mt-1">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center">
|
||||
<template v-if="isCurrentAccount(account.account_name) && currentAccountInfo?.avatar">
|
||||
<img :src="currentAccountInfo.avatar" class="w-14 h-14 rounded-xl border-2 border-[#07C160]/30 mr-4 shadow-sm object-cover bg-white" alt=""/>
|
||||
<img :src="currentAccountInfo.avatar" class="w-12 h-12 rounded-xl border-2 border-[#07C160]/30 mr-3 object-cover bg-white" alt=""/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="w-14 h-14 bg-gradient-to-br from-[#07C160]/10 to-[#91D300]/10 rounded-xl flex items-center justify-center mr-4 shadow-inner">
|
||||
<span class="text-[#07C160] font-bold text-xl">{{ account.account_name?.charAt(0)?.toUpperCase() || 'U' }}</span>
|
||||
<div class="w-12 h-12 bg-gradient-to-br from-[#07C160]/10 to-[#91D300]/10 rounded-xl flex items-center justify-center mr-3">
|
||||
<span class="text-[#07C160] font-bold text-lg">{{ account.account_name?.charAt(0)?.toUpperCase() || 'U' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<div class="flex flex-col">
|
||||
<template v-if="isCurrentAccount(account.account_name) && currentAccountInfo?.nickname">
|
||||
<p class="text-xl font-extrabold text-[#000000e6] leading-tight">{{ currentAccountInfo.nickname }}</p>
|
||||
<p class="text-xs text-[#7F7F7F] mt-1 font-mono">wxid: {{ account.account_name }}</p>
|
||||
<p class="text-lg font-bold text-[#000000e6] leading-tight">{{ currentAccountInfo.nickname }}</p>
|
||||
<p class="text-[11px] text-[#7F7F7F] mt-0.5 font-mono">wxid: {{ account.account_name }}</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p class="text-lg font-bold text-[#000000e6]">{{ account.account_name || '未知账户' }}</p>
|
||||
<p class="text-[15px] font-semibold text-[#000000e6]">{{ account.account_name || '未知账户' }}</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center mt-2 space-x-4 text-sm text-[#7F7F7F]">
|
||||
<div class="flex items-center mt-1.5 space-x-3 text-[12px] text-[#7F7F7F]">
|
||||
<span class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-1 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4"/>
|
||||
@@ -227,7 +260,7 @@
|
||||
</div>
|
||||
|
||||
<button @click="goToDecrypt(account)"
|
||||
class="inline-flex items-center px-6 py-2.5 bg-[#07C160] text-white rounded-xl font-bold hover:bg-[#06AD56] hover:shadow-md hover:-translate-y-0.5 transition-all duration-200 text-sm shrink-0 z-10">
|
||||
class="inline-flex items-center px-4 py-2 bg-[#07C160] text-white rounded-lg font-semibold hover:bg-[#06AD56] hover:-translate-y-0.5 transition-all duration-200 text-xs shrink-0 z-10">
|
||||
解密提取
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
@@ -235,8 +268,8 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-3 border-t border-dashed border-gray-200 text-sm text-gray-400">
|
||||
<p v-if="account.data_dir" class="font-mono text-xs truncate" title="复制路径">
|
||||
<div class="mt-3 pt-2.5 border-t border-dashed border-gray-200 text-sm text-gray-400">
|
||||
<p v-if="account.data_dir" class="font-mono text-[11px] truncate" title="复制路径">
|
||||
📂 {{ account.data_dir }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -245,11 +278,11 @@
|
||||
</div>
|
||||
|
||||
<!-- 无账户提示 -->
|
||||
<div v-else class="bg-white rounded-2xl p-12 text-center border border-[#EDEDED]">
|
||||
<svg class="w-16 h-16 mx-auto text-gray-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div v-else class="bg-white rounded-2xl p-8 text-center border border-[#EDEDED]">
|
||||
<svg class="w-12 h-12 mx-auto text-gray-300 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<p class="text-lg text-[#000000e6] font-medium">没有在这台设备上发现微信数据</p>
|
||||
<p class="text-base text-[#000000e6] font-medium">没有在这台设备上发现微信数据</p>
|
||||
<p class="text-sm text-gray-500 mt-2">您可以尝试通过上方的按钮手动指定 "xwechat_files" 文件夹路径。</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -315,7 +348,6 @@ const handlePickDirectory = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 计算属性:将当前登录账号排在第一位
|
||||
const persistWechatInstallPath = () => {
|
||||
const normalized = normalizeWechatInstallPath(wechatInstallPath.value)
|
||||
wechatInstallPath.value = normalized
|
||||
@@ -361,7 +393,7 @@ const pickWechatInstallDirectory = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// ?????????????????
|
||||
// 计算属性:将当前登录账号排在第一位
|
||||
const sortedAccounts = computed(() => {
|
||||
if (!detectionResult.value?.data?.accounts) return []
|
||||
const accounts = [...detectionResult.value.data.accounts]
|
||||
|
||||
+217
-139
@@ -1,134 +1,186 @@
|
||||
<template>
|
||||
<div class="import-page min-h-screen flex items-center justify-center py-8">
|
||||
|
||||
<div class="max-w-2xl mx-auto px-6 w-full">
|
||||
<div class="bg-white rounded-3xl border border-[#EDEDED] shadow-sm overflow-hidden">
|
||||
<div class="p-8 md:p-12">
|
||||
<!-- 标题部分 -->
|
||||
<div class="flex items-center mb-8">
|
||||
<div class="w-14 h-14 bg-[#91D300] rounded-2xl flex items-center justify-center mr-5 shadow-sm">
|
||||
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-[#000000e6]">数据导入</h2>
|
||||
<p class="text-[#7F7F7F] mt-1">导入已解密的数据库备份目录</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="import-page min-h-screen relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-grid-pattern opacity-5 pointer-events-none"></div>
|
||||
|
||||
<div class="bg-blue-50 border border-blue-100 rounded-2xl p-6 mb-8">
|
||||
<h3 class="text-blue-800 font-bold mb-3 flex items-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
标准目录结构要求
|
||||
</h3>
|
||||
<ul class="text-sm text-blue-700 space-y-2 list-disc list-inside opacity-90">
|
||||
<li><strong>预期目标:</strong>请选择形如 <strong>/output/wxid_xxxxx/</strong> 这一级目录</li>
|
||||
<li><strong>databases/</strong> 目录:存放扁平化的 .db 文件</li>
|
||||
<li><strong>account.json</strong> 文件:系统会自动生成</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 初始状态:选择目录 -->
|
||||
<div v-if="!importPreview && !importError && !importing" class="flex flex-col items-center justify-center py-12 border-2 border-dashed border-[#EDEDED] rounded-3xl hover:border-[#91D300] transition-colors cursor-pointer group" @click="handlePickDirectory">
|
||||
<div class="w-20 h-20 bg-gray-50 rounded-full flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300">
|
||||
<svg class="w-10 h-10 text-gray-400 group-hover:text-[#91D300]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-[#000000e6] font-medium text-lg">点击选择备份目录</div>
|
||||
<p class="text-[#7F7F7F] text-sm mt-2">支持原生目录选择器</p>
|
||||
</div>
|
||||
|
||||
<!-- 导入进度状态 -->
|
||||
<div v-if="importing" class="animate-fade-in py-12">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="relative w-32 h-32 mb-8">
|
||||
<svg class="w-full h-full" viewBox="0 0 100 100">
|
||||
<circle class="text-gray-100" stroke-width="8" stroke="currentColor" fill="transparent" r="42" cx="50" cy="50"/>
|
||||
<circle class="text-[#91D300] transition-all duration-500" stroke-width="8" :stroke-dasharray="263.89" :stroke-dashoffset="263.89 * (1 - importProgress / 100)" stroke-linecap="round" stroke="currentColor" fill="transparent" r="42" cx="50" cy="50" transform="rotate(-90 50 50)"/>
|
||||
<div class="relative z-10 mx-auto flex min-h-screen w-full max-w-3xl items-center justify-center px-4 py-6 sm:px-6 sm:py-8">
|
||||
<div class="w-full rounded-[28px] border border-[#EDEDED] bg-white/92 backdrop-blur-sm">
|
||||
<div class="px-5 py-5 sm:px-7 sm:py-7">
|
||||
<div class="mb-5 flex items-start justify-between gap-3">
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<div class="flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-[#07C160]/10 text-[#07C160]">
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<span class="text-2xl font-bold text-gray-900">{{ importProgress }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-bold text-gray-900 mb-2">{{ importMessage }}</h3>
|
||||
<p class="text-sm text-gray-500">正在为您处理数据,请稍候...</p>
|
||||
|
||||
<div class="w-full max-w-xs bg-gray-100 h-1.5 rounded-full mt-8 overflow-hidden">
|
||||
<div class="bg-[#91D300] h-full transition-all duration-500" :style="{ width: importProgress + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预览状态:显示账号信息 -->
|
||||
<div v-if="importPreview && !importing" class="animate-fade-in">
|
||||
<div class="flex flex-col items-center py-8 bg-[#FBFBFB] rounded-3xl border border-[#EDEDED] mb-8">
|
||||
<div class="w-28 h-28 rounded-full overflow-hidden border-4 border-white shadow-md mb-5">
|
||||
<img :src="importPreview.avatar_url || '/Contact.png'" class="w-full h-full object-cover" alt="头像">
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-xl font-bold text-gray-900">{{ importPreview.nick }}</div>
|
||||
<div class="text-sm text-gray-500 font-mono mt-1 bg-white px-3 py-1 rounded-full border border-[#EDEDED] inline-block">{{ importPreview.username }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex gap-6">
|
||||
<div class="flex items-center text-sm text-gray-600">
|
||||
<div class="w-2 h-2 rounded-full bg-[#07C160] mr-2"></div>
|
||||
数据库已就绪
|
||||
</div>
|
||||
<div class="flex items-center text-sm text-gray-600">
|
||||
<div class="w-2 h-2 rounded-full mr-2" :class="importPreview.has_resource ? 'bg-[#07C160]' : 'bg-gray-300'"></div>
|
||||
资源文件{{ importPreview.has_resource ? '已发现' : '未发现' }}
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-[11px] uppercase tracking-[0.12em] text-[#7F7F7F]">Import backup</p>
|
||||
<h1 class="mt-1 text-[24px] font-semibold leading-none text-[#000000e6]">数据导入</h1>
|
||||
<p class="mt-2 text-sm text-[#7F7F7F]">导入已解密的微信备份目录,确认账号后即可写入当前工具。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<button @click="resetImport"
|
||||
class="flex-1 px-8 py-4 border border-[#EDEDED] text-gray-600 rounded-2xl font-bold hover:bg-gray-50 transition-all">
|
||||
重新选择
|
||||
</button>
|
||||
<button @click="confirmImport" :disabled="importing"
|
||||
class="flex-[2] px-8 py-4 bg-[#91D300] text-white rounded-2xl font-bold hover:bg-[#82BD00] shadow-lg shadow-[#91D300]/20 disabled:opacity-50 transition-all flex items-center justify-center transform hover:scale-[1.02] active:scale-[0.98]">
|
||||
<span v-if="!importing">确认导入此账号</span>
|
||||
<span v-else>正在导入数据...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-if="importError && !importing" class="animate-fade-in">
|
||||
<div class="p-6 bg-red-50 border border-red-100 rounded-2xl flex items-start mb-8">
|
||||
<svg class="w-6 h-6 text-red-500 mr-3 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="font-bold text-red-800 mb-1">导入失败</p>
|
||||
<p class="text-sm text-red-600">{{ importError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="resetImport"
|
||||
class="w-full px-8 py-4 bg-white border border-[#EDEDED] text-[#07C160] rounded-2xl font-bold hover:bg-gray-50 transition-all flex items-center justify-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
重新选择目录
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 返回首页 -->
|
||||
<div class="mt-12 text-center">
|
||||
<NuxtLink to="/" class="text-[#7F7F7F] hover:text-[#07C160] text-sm transition-colors inline-flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
<NuxtLink
|
||||
to="/"
|
||||
class="inline-flex shrink-0 items-center rounded-lg px-3 py-1.5 text-xs font-medium text-[#07C160] transition-colors hover:bg-[#F3FBF6] hover:text-[#06AD56]"
|
||||
>
|
||||
<svg class="mr-1 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
返回首页
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div class="mb-5 rounded-[22px] border border-[#E8EFE8] bg-[#F8FBF8] px-4 py-4">
|
||||
<div class="flex items-center gap-2 text-[13px] font-semibold text-[#000000d9]">
|
||||
<svg class="h-4 w-4 text-[#07C160]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>目录要求</span>
|
||||
</div>
|
||||
<div class="mt-3 grid gap-2 sm:grid-cols-3">
|
||||
<div class="rounded-2xl border border-white bg-white/80 px-3 py-3">
|
||||
<p class="text-[11px] uppercase tracking-[0.08em] text-[#7F7F7F]">Target</p>
|
||||
<p class="mt-1 text-sm leading-6 text-[#000000d9]">请选择 `output / wxid_xxxxx` 这一层目录。</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-white bg-white/80 px-3 py-3">
|
||||
<p class="text-[11px] uppercase tracking-[0.08em] text-[#7F7F7F]">Database</p>
|
||||
<p class="mt-1 text-sm leading-6 text-[#000000d9]">目录内需要包含 `databases/`,用于存放 `.db` 文件。</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-white bg-white/80 px-3 py-3">
|
||||
<p class="text-[11px] uppercase tracking-[0.08em] text-[#7F7F7F]">Account</p>
|
||||
<p class="mt-1 text-sm leading-6 text-[#000000d9]">`account.json` 会作为账号识别与信息校验依据。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!importPreview && !importError && !importing" class="animate-fade-in">
|
||||
<div
|
||||
class="group cursor-pointer rounded-[24px] border border-dashed border-[#D8E5DA] bg-[#FCFDFC] px-6 py-10 text-center transition-colors duration-200 hover:border-[#07C160] hover:bg-white"
|
||||
@click="handlePickDirectory"
|
||||
>
|
||||
<div class="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl bg-white text-[#07C160] ring-1 ring-[#EDEDED]">
|
||||
<svg class="h-7 w-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="mt-4 text-lg font-semibold text-[#000000e6]">选择解密备份目录</h3>
|
||||
<p class="mt-2 text-sm text-[#7F7F7F]">建议直接选择 `wxid_xxxxx` 层级,减少后续校验失败。</p>
|
||||
<div class="mt-5 inline-flex items-center rounded-full bg-[#07C160] px-4 py-2 text-sm font-medium text-white transition-colors duration-200 group-hover:bg-[#06AD56]">
|
||||
点击开始选择
|
||||
</div>
|
||||
<p class="mt-4 text-xs text-[#A3A3A3]">桌面端优先使用系统目录选择器,异常时会自动回退到手动输入。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="importing" class="animate-fade-in">
|
||||
<div class="rounded-[24px] border border-[#EDEDED] bg-[#FCFDFC] px-5 py-8 sm:px-6">
|
||||
<div class="mx-auto flex w-fit items-center gap-2 rounded-full bg-[#07C160]/10 px-3 py-1 text-xs font-medium text-[#07C160]">
|
||||
<span class="inline-flex h-2 w-2 rounded-full bg-current animate-pulse"></span>
|
||||
正在导入
|
||||
</div>
|
||||
|
||||
<div class="mt-5 text-center">
|
||||
<p class="text-xl font-semibold text-[#000000e6]">{{ importMessage }}</p>
|
||||
<p class="mt-2 text-sm text-[#7F7F7F]">请保持程序运行,导入完成后会自动进入聊天页面。</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 overflow-hidden rounded-full bg-[#EDF3EE]">
|
||||
<div
|
||||
class="h-2 rounded-full bg-[#07C160] transition-all duration-500"
|
||||
:style="{ width: `${Math.min(Math.max(importProgress, 0), 100)}%` }"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex items-center justify-between text-xs text-[#7F7F7F]">
|
||||
<span>已连接导入任务</span>
|
||||
<span>{{ importProgress }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="importPreview && !importing" class="animate-fade-in space-y-4">
|
||||
<div class="rounded-[24px] border border-[#EDEDED] bg-[#FCFDFC] px-5 py-5 sm:px-6">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex min-w-0 items-center gap-4">
|
||||
<div class="h-16 w-16 shrink-0 overflow-hidden rounded-2xl border border-[#EDEDED] bg-white">
|
||||
<img :src="importPreview.avatar_url || '/Contact.png'" class="h-full w-full object-cover" alt="Avatar" />
|
||||
</div>
|
||||
|
||||
<div class="min-w-0">
|
||||
<p class="text-[11px] uppercase tracking-[0.12em] text-[#7F7F7F]">Detected account</p>
|
||||
<div class="mt-1 truncate text-xl font-semibold text-[#000000e6]">{{ importPreview.nick || '未命名账号' }}</div>
|
||||
<div class="mt-2 inline-flex max-w-full items-center rounded-full border border-[#EDEDED] bg-white px-3 py-1 text-xs font-mono text-[#7F7F7F]">
|
||||
<span class="truncate">{{ importPreview.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex w-fit items-center rounded-full bg-[#07C160]/10 px-3 py-1 text-xs font-medium text-[#07C160]">
|
||||
可导入
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedImportPath" class="mt-4 rounded-[18px] border border-[#EDEDED] bg-white px-3 py-3">
|
||||
<p class="text-[11px] uppercase tracking-[0.12em] text-[#7F7F7F]">Import path</p>
|
||||
<p class="mt-1 break-all text-sm text-[#000000d9]">{{ selectedImportPath }}</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
<span class="inline-flex items-center gap-2 rounded-full border border-[#EDEDED] bg-white px-3 py-1.5 text-xs text-[#4A4A4A]">
|
||||
<span class="h-2 w-2 rounded-full bg-[#07C160]"></span>
|
||||
数据库已就绪
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-2 rounded-full border border-[#EDEDED] bg-white px-3 py-1.5 text-xs text-[#4A4A4A]">
|
||||
<span class="h-2 w-2 rounded-full" :class="importPreview.has_resource ? 'bg-[#07C160]' : 'bg-[#C9D2CB]'"></span>
|
||||
资源文件{{ importPreview.has_resource ? '已发现' : '未发现' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-[minmax(0,1fr)_minmax(0,1.35fr)]">
|
||||
<button
|
||||
class="inline-flex items-center justify-center rounded-2xl border border-[#E2E2E2] bg-white px-4 py-3 text-sm font-medium text-[#4A4A4A] transition-colors hover:bg-[#F8F8F8]"
|
||||
@click="handlePickDirectory"
|
||||
>
|
||||
重新选择目录
|
||||
</button>
|
||||
<button
|
||||
:disabled="importing"
|
||||
class="inline-flex items-center justify-center rounded-2xl bg-[#07C160] px-4 py-3 text-sm font-medium text-white transition-colors hover:bg-[#06AD56] disabled:cursor-not-allowed disabled:bg-[#8FD9AE]"
|
||||
@click="confirmImport"
|
||||
>
|
||||
确认导入此账号
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="importError && !importing" class="animate-fade-in space-y-4">
|
||||
<div class="rounded-[22px] border border-[#F4D6D6] bg-[#FFF7F7] px-5 py-5">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-white text-[#E05A5A] ring-1 ring-[#F0D7D7]">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-base font-semibold text-[#B64545]">导入失败</p>
|
||||
<p class="mt-1 text-sm leading-6 text-[#9C5F5F]">{{ importError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedImportPath" class="mt-4 rounded-[18px] border border-[#F1E3E3] bg-white/80 px-3 py-3">
|
||||
<p class="text-[11px] uppercase tracking-[0.12em] text-[#B26B6B]">Current path</p>
|
||||
<p class="mt-1 break-all text-sm text-[#7A4B4B]">{{ selectedImportPath }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="inline-flex w-full items-center justify-center rounded-2xl border border-[#E2E2E2] bg-white px-4 py-3 text-sm font-medium text-[#4A4A4A] transition-colors hover:bg-[#F8F8F8]"
|
||||
@click="retryPickDirectory"
|
||||
>
|
||||
重新选择目录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -136,9 +188,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, onUnmounted} from 'vue'
|
||||
import {useApi} from '~/composables/useApi'
|
||||
import {useApiBase} from '~/composables/useApiBase'
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
import { useApi } from '~/composables/useApi'
|
||||
import { useApiBase } from '~/composables/useApiBase'
|
||||
|
||||
const importing = ref(false)
|
||||
const importProgress = ref(0)
|
||||
@@ -149,10 +201,15 @@ const selectedImportPath = ref('')
|
||||
|
||||
let eventSource = null
|
||||
|
||||
onUnmounted(() => {
|
||||
const closeEventSource = () => {
|
||||
if (eventSource) {
|
||||
eventSource.close()
|
||||
eventSource = null
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
closeEventSource()
|
||||
})
|
||||
|
||||
const isDesktopShell = () => {
|
||||
@@ -161,6 +218,7 @@ const isDesktopShell = () => {
|
||||
}
|
||||
|
||||
const resetImport = () => {
|
||||
closeEventSource()
|
||||
importPreview.value = null
|
||||
importError.value = ''
|
||||
selectedImportPath.value = ''
|
||||
@@ -199,7 +257,10 @@ const handlePickDirectory = async () => {
|
||||
}
|
||||
|
||||
if (path && !path.includes('wxid_')) {
|
||||
const isOk = window.confirm(`你选择的目录为:\n${path}\n\n该目录似乎不符合 "wxid_xxxxx" 的格式。确定要继续吗?`)
|
||||
const isOk = window.confirm(`你选择的目录为:
|
||||
${path}
|
||||
|
||||
该目录似乎不符合 "wxid_xxxxx" 的格式。确定要继续吗?`)
|
||||
if (!isOk) return
|
||||
}
|
||||
|
||||
@@ -208,15 +269,20 @@ const handlePickDirectory = async () => {
|
||||
importPreview.value = null
|
||||
|
||||
try {
|
||||
importPreview.value = await importDecryptedPreview({import_path: path})
|
||||
importPreview.value = await importDecryptedPreview({ import_path: path })
|
||||
} catch (e) {
|
||||
importError.value = e.message || '目录格式不正确,请确保包含 databases 目录和 account.json'
|
||||
}
|
||||
}
|
||||
|
||||
const retryPickDirectory = async () => {
|
||||
resetImport()
|
||||
await handlePickDirectory()
|
||||
}
|
||||
|
||||
const confirmImport = async () => {
|
||||
if (!selectedImportPath.value) return
|
||||
|
||||
|
||||
importing.value = true
|
||||
importError.value = ''
|
||||
importProgress.value = 0
|
||||
@@ -225,30 +291,28 @@ const confirmImport = async () => {
|
||||
const url = new URL(`${apiBase.replace(/\/$/, '')}/import_decrypted`, window.location.origin)
|
||||
url.searchParams.set('import_path', selectedImportPath.value)
|
||||
|
||||
if (eventSource) eventSource.close()
|
||||
|
||||
closeEventSource()
|
||||
eventSource = new EventSource(url.toString())
|
||||
|
||||
|
||||
eventSource.onmessage = async (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
|
||||
|
||||
if (data.type === 'progress') {
|
||||
importProgress.value = data.percent || 0
|
||||
importMessage.value = data.message || '正在处理...'
|
||||
} else if (data.type === 'complete') {
|
||||
importProgress.value = 100
|
||||
importMessage.value = '导入完成!'
|
||||
eventSource.close()
|
||||
|
||||
// 延迟跳转,让用户看到 100%
|
||||
closeEventSource()
|
||||
|
||||
setTimeout(async () => {
|
||||
await navigateTo('/chat')
|
||||
}, 1000)
|
||||
} else if (data.type === 'error') {
|
||||
importError.value = data.message || '导入失败'
|
||||
importing.value = false
|
||||
eventSource.close()
|
||||
closeEventSource()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析 SSE 数据失败:', e)
|
||||
@@ -259,18 +323,32 @@ const confirmImport = async () => {
|
||||
console.error('EventSource 错误:', e)
|
||||
importError.value = '与服务器连接断开或发生错误'
|
||||
importing.value = false
|
||||
eventSource.close()
|
||||
closeEventSource()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
animation: fadeIn 0.35s ease-out;
|
||||
}
|
||||
|
||||
.bg-grid-pattern {
|
||||
background-image:
|
||||
linear-gradient(rgba(7, 193, 96, 0.08) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(7, 193, 96, 0.08) 1px, transparent 1px);
|
||||
background-size: 32px 32px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -34,13 +34,6 @@
|
||||
<span>开始检测</span>
|
||||
</button>
|
||||
|
||||
<NuxtLink to="/decrypt"
|
||||
class="group inline-flex items-center px-12 py-4 bg-white text-[#07C160] border border-[#07C160] rounded-lg text-lg font-medium hover:bg-[#F7F7F7] transform hover:scale-105 transition-all duration-200">
|
||||
<svg class="w-6 h-6 mr-3 group-hover:-rotate-12 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<span>直接解密</span>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink to="/import"
|
||||
class="group inline-flex items-center px-12 py-4 bg-white text-[#91D300] border border-[#91D300] rounded-lg text-lg font-medium hover:bg-[#F7F7F7] transform hover:scale-105 transition-all duration-200">
|
||||
|
||||
Reference in New Issue
Block a user