feat(frontend): 添加前端页面

This commit is contained in:
2977094657
2025-07-25 20:21:26 +08:00
parent 0b12e31c96
commit 540a0fd823
21 changed files with 15094 additions and 13 deletions

1
.gitignore vendored
View File

@@ -12,3 +12,4 @@ wheels/
/.idea
/.history/
/.augment/
/CLAUDE.md

View File

@@ -6,23 +6,19 @@
一个专门用于微信4.x版本数据库解密的工具
## 🚧 项目开发状态
## 🚀 功能特性
**本项目目前正处于开发阶段**
### 当前已实现功能
### 已实现功能
-**数据库解密**: 支持微信4.x版本数据库文件的解密
-**多账户检测**: 自动检测并处理多个微信账户的数据库文件
-**API接口**: 提供RESTful API接口进行数据库解密操作
-**Web界面**: 提供现代化的Web操作界面
### 后续开发计划
- 🔄 **Web界面**: 提供友好的Web操作界面
### 开发计划
- 🔄 **数据分析**: 对解密后的数据进行深度分析
- 🔄 **数据可视化**: 提供图表、统计报告等可视化展示
- 🔄 **聊天记录分析**: 消息频率、活跃时间、关键词分析等
**欢迎关注项目更新,更多功能正在开发中!**
## 快速开始
### 1. 克隆项目
@@ -31,23 +27,38 @@
git clone https://github.com/2977094657/WeChatDataAnalysis
```
### 2. 安装Python依赖
### 2. 安装后端依赖
```bash
# 使用uv (推荐)
uv sync
```
### 3. 启动API服务
### 3. 安装前端依赖
```bash
cd frontend
npm install
```
### 4. 启动服务
#### 启动后端API服务
```bash
# 在项目根目录
uv run main.py
```
**注意**: 服务将在8000端口启动支持热重载
#### 启动前端开发服务器
```bash
# 在frontend目录
cd frontend
npm run dev
```
### 4. 访问应用
### 5. 访问应用
- 前端界面: http://localhost:3000
- API服务: http://localhost:8000
- API文档: http://localhost:8000/docs

24
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

18
frontend/app.vue Normal file
View File

@@ -0,0 +1,18 @@
<template>
<div class="min-h-screen bg-gradient-to-br from-green-50 via-emerald-50 to-green-100">
<NuxtPage />
</div>
</template>
<style>
/* 页面过渡动画 - 渐显渐隐效果 */
.page-enter-active,
.page-leave-active {
transition: opacity 0.3s ease;
}
.page-enter-from,
.page-leave-to {
opacity: 0;
}
</style>

View File

@@ -0,0 +1,163 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* 自定义全局样式 - 微信配色主题 */
@layer base {
:root {
/* 微信品牌色 */
--wechat-green: #07c160;
--wechat-green-hover: #06ad56;
--wechat-green-light: #e6f7f0;
--wechat-green-dark: #059341;
/* 主色调 */
--primary-color: #07c160;
--primary-hover: #06ad56;
--secondary-color: #4c9e5f;
/* 危险色 */
--danger-color: #fa5151;
--danger-hover: #e94848;
/* 警告色 */
--warning-color: #ffc300;
--warning-hover: #e6ad00;
/* 背景色 */
--bg-primary: #f7f8fa;
--bg-secondary: #ffffff;
--bg-gray: #ededed;
--bg-dark: #191919;
/* 文字颜色 */
--text-primary: #191919;
--text-secondary: #576b95;
--text-light: #888888;
--text-white: #ffffff;
/* 边框颜色 */
--border-color: #e7e7e7;
--border-light: #f4f4f4;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
/* 微信风格组件样式 */
@layer components {
/* 按钮样式 */
.btn {
@apply px-6 py-3 rounded-full font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 transform active:scale-95;
}
.btn-primary {
@apply bg-[#07c160] text-white hover:bg-[#06ad56] focus:ring-[#07c160] shadow-md hover:shadow-lg;
}
.btn-secondary {
@apply bg-white text-[#07c160] border-2 border-[#07c160] hover:bg-[#e6f7f0] focus:ring-[#07c160];
}
.btn-danger {
@apply bg-[#fa5151] text-white hover:bg-[#e94848] focus:ring-[#fa5151] shadow-md hover:shadow-lg;
}
.btn-ghost {
@apply bg-transparent text-[#576b95] hover:bg-gray-100 focus:ring-gray-300;
}
/* 卡片样式 */
.card {
@apply bg-white rounded-2xl shadow-sm border border-[#f4f4f4] p-6 hover:shadow-md transition-shadow duration-300;
}
.card-hover {
@apply hover:transform hover:scale-[1.02] transition-all duration-300;
}
/* 输入框样式 */
.input {
@apply w-full px-4 py-3 bg-[#f7f8fa] border border-transparent rounded-xl focus:outline-none focus:ring-2 focus:ring-[#07c160] focus:bg-white focus:border-[#07c160] transition-all duration-200;
}
.input-error {
@apply border-[#fa5151] focus:ring-[#fa5151] focus:border-[#fa5151];
}
/* 标签样式 */
.tag {
@apply inline-flex items-center px-3 py-1 rounded-full text-xs font-medium;
}
.tag-green {
@apply bg-[#e6f7f0] text-[#07c160];
}
.tag-blue {
@apply bg-blue-100 text-blue-700;
}
.tag-red {
@apply bg-red-100 text-red-700;
}
/* 加载动画 */
.loading-spinner {
@apply inline-block w-8 h-8 border-4 border-[#e7e7e7] border-t-[#07c160] rounded-full animate-spin;
}
.loading-dots {
@apply inline-flex items-center space-x-1;
}
.loading-dots span {
@apply w-2 h-2 bg-[#07c160] rounded-full animate-bounce;
}
/* 微信风格的列表项 */
.list-item {
@apply flex items-center justify-between py-4 px-4 hover:bg-[#f7f8fa] transition-colors duration-200 cursor-pointer;
}
/* 分割线 */
.divider {
@apply border-t border-[#f4f4f4] my-4;
}
/* 提示框 */
.alert {
@apply p-4 rounded-xl border;
}
.alert-success {
@apply bg-[#e6f7f0] border-[#07c160] text-[#059341];
}
.alert-warning {
@apply bg-yellow-50 border-[#ffc300] text-yellow-800;
}
.alert-error {
@apply bg-red-50 border-[#fa5151] text-red-800;
}
/* 动画效果 */
.fade-enter {
@apply opacity-0 transform scale-95;
}
.fade-enter-active {
@apply transition-all duration-300 ease-out;
}
.fade-enter-to {
@apply opacity-100 transform scale-100;
}
}

View File

@@ -0,0 +1,21 @@
<template>
<div v-if="appStore.apiStatus !== 'connected'"
class="fixed top-4 right-4 bg-red-50 border border-red-200 rounded-lg p-4 shadow-lg max-w-sm z-50">
<div class="flex items-start">
<svg class="h-5 w-5 text-red-600 mr-2 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>
<h4 class="text-red-800 font-semibold">API连接问题</h4>
<p class="text-red-700 text-sm mt-1">{{ appStore.apiMessage || '无法连接到后端服务' }}</p>
<p class="text-red-600 text-xs mt-2">请确保后端服务正在运行 (端口: 8000)</p>
</div>
</div>
</div>
</template>
<script setup>
import { useAppStore } from '~/stores/app'
const appStore = useAppStore()
</script>

View File

@@ -0,0 +1,52 @@
// API请求组合式函数
export const useApi = () => {
const config = useRuntimeConfig()
// 基础请求函数
const request = async (url, options = {}) => {
try {
// 在客户端使用完整的API路径
const baseURL = process.client ? 'http://localhost:8000/api' : '/api'
const response = await $fetch(url, {
baseURL,
...options,
onResponseError({ response }) {
if (response.status === 400) {
throw new Error(response._data?.detail || '请求参数错误')
} else if (response.status === 500) {
throw new Error('服务器错误,请稍后重试')
}
}
})
return response
} catch (error) {
console.error('API请求错误:', error)
throw error
}
}
// 微信检测API
const detectWechat = async () => {
return await request('/wechat-detection')
}
// 数据库解密API
const decryptDatabase = async (data) => {
return await request('/decrypt', {
method: 'POST',
body: data
})
}
// 健康检查API
const healthCheck = async () => {
return await request('/health')
}
return {
detectWechat,
decryptDatabase,
healthCheck
}
}

50
frontend/nuxt.config.ts Normal file
View File

@@ -0,0 +1,50 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: { enabled: false },
// 配置前端开发服务器端口
devServer: {
port: 3000
},
// 配置API代理解决跨域问题
nitro: {
devProxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
},
// 应用配置
app: {
head: {
title: '微信数据库解密工具',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ name: 'description', content: '微信4.x版本数据库解密工具' }
]
},
pageTransition: { name: 'page', mode: 'out-in' }
},
// 模块配置
modules: [
'@nuxtjs/tailwindcss',
'@pinia/nuxt'
],
// Tailwind配置
tailwindcss: {
cssPath: ['~/assets/css/tailwind.css', { injectPosition: "first" }],
configPath: 'tailwind.config',
exposeConfig: {
level: 2
},
config: {},
viewer: true
}
})

13525
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
frontend/package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxtjs/tailwindcss": "^6.14.0",
"@pinia/nuxt": "^0.11.2",
"axios": "^1.11.0",
"nuxt": "^4.0.1",
"vue": "^3.5.17",
"vue-router": "^4.5.1"
}
}

View File

@@ -0,0 +1,174 @@
<template>
<div class="min-h-screen relative overflow-hidden flex items-center justify-center">
<!-- 网格背景 -->
<div class="absolute inset-0 bg-grid-pattern opacity-5 pointer-events-none"></div>
<!-- 装饰元素 -->
<div class="absolute top-20 left-20 w-72 h-72 bg-[#07C160] opacity-5 rounded-full blur-3xl pointer-events-none"></div>
<div class="absolute top-40 right-20 w-96 h-96 bg-[#10AEEF] opacity-5 rounded-full blur-3xl pointer-events-none"></div>
<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-4xl mx-auto px-4">
<!-- 成功卡片 -->
<div class="bg-white rounded-2xl border border-[#EDEDED] p-8 text-center">
<!-- 成功图标 -->
<div class="mb-4">
<div class="w-20 h-20 bg-[#07C160]/10 rounded-full flex items-center justify-center mx-auto">
<svg class="w-10 h-10 text-[#07C160]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/>
</svg>
</div>
</div>
<!-- 标题 -->
<h2 class="text-2xl font-bold text-[#000000e6] mb-6">解密完成</h2>
<!-- 统计信息 -->
<div class="flex justify-center gap-8 mb-6">
<div>
<div class="text-3xl font-bold text-[#10AEEF]">{{ decryptResult?.total_databases || 0 }}</div>
<div class="text-sm text-[#7F7F7F]">总数据库</div>
</div>
<div class="border-l border-[#EDEDED]"></div>
<div>
<div class="text-3xl font-bold text-[#07C160]">{{ decryptResult?.success_count || 0 }}</div>
<div class="text-sm text-[#7F7F7F]">成功解密</div>
</div>
<div class="border-l border-[#EDEDED]"></div>
<div>
<div class="text-3xl font-bold text-[#FA5151]">{{ decryptResult?.failure_count || 0 }}</div>
<div class="text-sm text-[#7F7F7F]">解密失败</div>
</div>
</div>
<!-- 输出目录 -->
<div class="bg-gray-50 rounded-lg p-4 mb-6">
<div class="flex items-center justify-center">
<svg class="w-5 h-5 text-[#7F7F7F] 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>
<span class="text-sm text-[#7F7F7F] mr-2">输出目录</span>
<code class="bg-white px-3 py-1 rounded text-sm font-mono text-[#000000e6] border border-[#EDEDED]">
{{ decryptResult?.output_directory || '-' }}
</code>
<button v-if="decryptResult?.output_directory"
@click="copyPath"
class="ml-2 text-[#07C160] hover:text-[#06AD56] transition-colors group relative"
:title="copyTooltip">
<svg v-if="!copied" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>
<svg v-else class="w-5 h-5 text-[#07C160]" 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 v-if="copied" class="absolute -top-8 left-1/2 transform -translate-x-1/2 bg-[#07C160] text-white text-xs px-2 py-1 rounded whitespace-nowrap">
已复制
</span>
</button>
</div>
</div>
<!-- 提示信息 -->
<p class="text-sm text-[#7F7F7F] mb-6">
解密后的数据库文件已保存您可以使用SQLite工具查看
</p>
<!-- 操作按钮 -->
<div class="flex justify-center gap-4">
<NuxtLink to="/decrypt"
class="inline-flex items-center px-6 py-3 bg-[#07C160] text-white rounded-lg font-medium hover:bg-[#06AD56] transition-all duration-200">
<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>
继续解密
</NuxtLink>
<a href="https://sqlitebrowser.org/" target="_blank"
class="inline-flex items-center px-6 py-3 bg-white text-[#07C160] border border-[#07C160] rounded-lg font-medium hover:bg-gray-50 transition-all duration-200">
<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 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
</svg>
下载SQLite Browser
</a>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const decryptResult = ref(null)
const copied = ref(false)
const copyTooltip = ref('复制路径')
// 复制路径
const copyPath = async () => {
if (!decryptResult.value?.output_directory) return
try {
// 获取用户文件夹路径
// 如果有多个账户,显示基础路径
// 如果只有一个账户,可以显示具体到账户的路径
let pathToCopy = decryptResult.value.output_directory
// 如果只解密了一个账户的数据,且有账户结果信息
if (decryptResult.value.account_results) {
const accounts = Object.keys(decryptResult.value.account_results)
if (accounts.length === 1) {
// 如果只有一个账户,直接显示该账户的输出目录
const accountName = accounts[0]
pathToCopy = `${pathToCopy}\\${accountName}`
}
}
await navigator.clipboard.writeText(pathToCopy)
copied.value = true
copyTooltip.value = '已复制'
// 2秒后重置状态
setTimeout(() => {
copied.value = false
copyTooltip.value = '复制路径'
}, 2000)
} catch (err) {
console.error('复制失败:', err)
}
}
// 页面加载时获取解密结果
onMounted(() => {
// 从sessionStorage获取解密结果
if (process.client && typeof window !== 'undefined') {
const result = sessionStorage.getItem('decryptResult')
if (result) {
try {
decryptResult.value = JSON.parse(result)
// 清除sessionStorage
sessionStorage.removeItem('decryptResult')
} catch (e) {
console.error('解析解密结果失败:', e)
}
}
}
// 如果没有解密结果,重定向到解密页面
if (!decryptResult.value) {
navigateTo('/decrypt')
}
})
</script>
<style scoped>
/* 网格背景 */
.bg-grid-pattern {
background-image:
linear-gradient(rgba(7, 193, 96, 0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(7, 193, 96, 0.1) 1px, transparent 1px);
background-size: 50px 50px;
}
</style>

252
frontend/pages/decrypt.vue Normal file
View File

@@ -0,0 +1,252 @@
<template>
<div class="min-h-screen flex items-center justify-center">
<div class="max-w-4xl mx-auto px-6 w-full">
<!-- 解密表单 -->
<div class="bg-white rounded-2xl border border-[#EDEDED]">
<div class="p-8">
<div class="flex items-center mb-6">
<div class="w-12 h-12 bg-[#07C160] rounded-lg flex items-center justify-center mr-4">
<svg class="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
</div>
<div>
<h2 class="text-xl font-bold text-[#000000e6]">解密配置</h2>
<p class="text-sm text-[#7F7F7F]">输入密钥和路径开始解密</p>
</div>
</div>
<form @submit.prevent="handleDecrypt" class="space-y-6">
<!-- 密钥输入 -->
<div>
<label for="key" class="block text-sm font-medium text-[#000000e6] mb-2">
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
</svg>
解密密钥 <span class="text-red-500">*</span>
</label>
<div class="relative">
<input
id="key"
v-model="formData.key"
type="text"
placeholder="请输入64位十六进制密钥"
class="w-full px-4 py-3 bg-white border border-[#EDEDED] rounded-lg font-mono text-sm focus:outline-none focus:ring-2 focus:ring-[#07C160] focus:border-transparent transition-all duration-200"
:class="{ 'border-red-500': formErrors.key }"
required
/>
<div v-if="formData.key" class="absolute right-3 top-1/2 transform -translate-y-1/2">
<span class="text-xs text-[#7F7F7F]">{{ formData.key.length }}/64</span>
</div>
</div>
<p v-if="formErrors.key" class="mt-1 text-sm text-red-600 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="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
{{ formErrors.key }}
</p>
<p class="mt-2 text-xs text-[#7F7F7F] flex items-center">
<svg class="w-4 h-4 mr-1 text-[#10AEEF]" 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>
使用 <a href="https://github.com/gzygood/DbkeyHook" target="_blank" class="text-[#07C160] hover:text-[#06AD56]">DbkeyHook</a> 等工具获取的64位十六进制字符串
</p>
</div>
<!-- 数据库路径输入 -->
<div>
<label for="dbPath" class="block text-sm font-medium text-[#000000e6] mb-2">
<svg class="w-4 h-4 inline mr-1" 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>
数据库存储路径 <span class="text-red-500">*</span>
</label>
<input
id="dbPath"
v-model="formData.db_storage_path"
type="text"
placeholder="例如: D:\wechatMSG\xwechat_files\wxid_xxx\db_storage"
class="w-full px-4 py-3 bg-white border border-[#EDEDED] rounded-lg font-mono text-sm focus:outline-none focus:ring-2 focus:ring-[#07C160] focus:border-transparent transition-all duration-200"
:class="{ 'border-red-500': formErrors.db_storage_path }"
required
/>
<p v-if="formErrors.db_storage_path" class="mt-1 text-sm text-red-600 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="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
{{ formErrors.db_storage_path }}
</p>
<p class="mt-2 text-xs text-[#7F7F7F] flex items-center">
<svg class="w-4 h-4 mr-1 text-[#10AEEF]" 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>
请输入数据库文件所在的绝对路径
</p>
</div>
<!-- 提交按钮 -->
<div class="pt-4 border-t border-[#EDEDED]">
<div class="flex items-center justify-center">
<button
type="submit"
:disabled="loading"
class="inline-flex items-center px-8 py-3 bg-[#07C160] text-white rounded-lg text-base font-medium hover:bg-[#06AD56] transform hover:scale-105 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg v-if="!loading" 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="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>
<svg v-if="loading" class="w-5 h-5 mr-2 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{{ loading ? '解密中...' : '开始解密' }}
</button>
</div>
</div>
</form>
</div>
</div>
<!-- 错误提示 -->
<transition name="fade">
<div v-if="error" class="bg-red-50 border border-red-200 rounded-lg p-4 mt-6 animate-shake flex items-start">
<svg class="h-5 w-5 mr-2 flex-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"/>
</svg>
<div>
<p class="font-semibold">解密失败</p>
<p class="text-sm mt-1">{{ error }}</p>
</div>
</div>
</transition>
</div>
</div>
</template>
<style scoped>
/* 动画效果 */
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
20%, 40%, 60%, 80% { transform: translateX(5px); }
}
.animate-shake {
animation: shake 0.5s ease-in-out;
}
</style>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useApi } from '~/composables/useApi'
const { decryptDatabase } = useApi()
const loading = ref(false)
const error = ref('')
// 表单数据
const formData = reactive({
key: '',
db_storage_path: ''
})
// 表单错误
const formErrors = reactive({
key: '',
db_storage_path: ''
})
// 验证表单
const validateForm = () => {
let isValid = true
formErrors.key = ''
formErrors.db_storage_path = ''
// 验证密钥
if (!formData.key) {
formErrors.key = '请输入解密密钥'
isValid = false
} else if (formData.key.length !== 64) {
formErrors.key = '密钥必须是64位十六进制字符串'
isValid = false
} else if (!/^[0-9a-fA-F]+$/.test(formData.key)) {
formErrors.key = '密钥必须是有效的十六进制字符串'
isValid = false
}
// 验证路径
if (!formData.db_storage_path) {
formErrors.db_storage_path = '请输入数据库存储路径'
isValid = false
}
return isValid
}
// 处理解密
const handleDecrypt = async () => {
if (!validateForm()) {
return
}
loading.value = true
error.value = ''
try {
const result = await decryptDatabase({
key: formData.key,
db_storage_path: formData.db_storage_path
})
if (result.status === 'completed') {
// 解密成功,跳转到结果页面
if (process.client && typeof window !== 'undefined') {
sessionStorage.setItem('decryptResult', JSON.stringify(result))
}
navigateTo('/decrypt-result')
} else if (result.status === 'failed') {
if (result.failure_count > 0 && result.success_count === 0) {
error.value = result.message || '所有文件解密失败'
} else {
error.value = '部分文件解密失败,请检查密钥是否正确'
}
} else {
error.value = result.message || '解密失败,请检查输入信息'
}
} catch (err) {
error.value = err.message || '解密过程中发生错误'
} finally {
loading.value = false
}
}
// 页面加载时检查是否有选中的账户
onMounted(() => {
if (process.client && typeof window !== 'undefined') {
const selectedAccount = sessionStorage.getItem('selectedAccount')
if (selectedAccount) {
try {
const account = JSON.parse(selectedAccount)
// 填充数据路径
if (account.data_dir) {
formData.db_storage_path = account.data_dir + '\\db_storage'
}
// 清除sessionStorage
sessionStorage.removeItem('selectedAccount')
} catch (e) {
console.error('解析账户信息失败:', e)
}
}
}
})
</script>

View File

@@ -0,0 +1,268 @@
<template>
<div class="min-h-screen relative overflow-hidden flex items-center">
<!-- 网格背景 -->
<div class="absolute inset-0 bg-grid-pattern opacity-5 pointer-events-none"></div>
<!-- 装饰元素 -->
<div class="absolute top-20 left-20 w-72 h-72 bg-[#07C160] opacity-5 rounded-full blur-3xl pointer-events-none"></div>
<div class="absolute top-40 right-20 w-96 h-96 bg-[#10AEEF] opacity-5 rounded-full blur-3xl pointer-events-none"></div>
<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="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>
<NuxtLink to="/"
class="inline-flex items-center px-3 py-1.5 text-sm text-[#07C160] hover:text-[#06AD56] 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>
返回首页
</NuxtLink>
</div>
<!-- 主内容区域 -->
<div>
<!-- 检测中状态 -->
<div v-if="loading" class="bg-white rounded-2xl p-12 text-center">
<svg class="w-16 h-16 mx-auto animate-spin text-[#07C160]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p class="mt-4 text-lg text-[#7F7F7F]">正在检测微信数据...</p>
</div>
<!-- 检测结果内容 -->
<div v-else-if="detectionResult">
<!-- 错误信息 -->
<div v-if="detectionResult.error" class="bg-white rounded-2xl border border-red-200 p-8">
<div class="flex items-center">
<svg class="w-8 h-8 text-red-500 mr-3" 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="text-lg font-medium text-red-600">检测失败</p>
<p class="text-red-500 mt-1">{{ detectionResult.error }}</p>
</div>
</div>
</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-if="detectionResult.data?.accounts && detectionResult.data.accounts.length > 0"
class="bg-white rounded-2xl border border-[#EDEDED] overflow-hidden">
<div class="p-4 border-b border-[#EDEDED] bg-gray-50">
<h3 class="text-base font-semibold text-[#000000e6]">微信账户详情</h3>
</div>
<div class="divide-y divide-[#EDEDED] max-h-64 overflow-y-auto">
<div v-for="(account, index) in detectionResult.data.accounts" :key="index"
class="p-4 hover:bg-gray-50 transition-all duration-200">
<div class="flex items-center justify-between">
<div class="flex-1">
<div class="flex items-center">
<div class="w-12 h-12 bg-gradient-to-br from-[#07C160]/10 to-[#91D300]/10 rounded-full flex items-center justify-center mr-4">
<span class="text-[#07C160] font-bold text-lg">{{ account.account_name?.charAt(0)?.toUpperCase() || 'U' }}</span>
</div>
<div>
<p class="text-lg font-medium text-[#000000e6]">{{ account.account_name || '未知账户' }}</p>
<div class="flex items-center mt-1 space-x-4 text-sm text-[#7F7F7F]">
<span class="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="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"/>
</svg>
{{ account.database_count }} 个数据库
</span>
<span v-if="account.data_dir" class="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="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
</svg>
数据目录已找到
</span>
</div>
</div>
</div>
</div>
<button @click="goToDecrypt(account)"
class="inline-flex items-center px-4 py-2 bg-[#07C160] text-white rounded-lg font-medium hover:bg-[#06AD56] transition-all duration-200 text-sm">
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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>
解密
</button>
</div>
<!-- 展开更多信息 -->
<div class="mt-4 text-sm text-[#7F7F7F]">
<p v-if="account.data_dir" class="font-mono text-xs truncate">
数据路径{{ account.data_dir }}
</p>
</div>
</div>
</div>
</div>
<!-- 无账户提示 -->
<div v-else class="bg-white rounded-2xl p-12 text-center">
<svg class="w-16 h-16 mx-auto text-[#7F7F7F] mb-4" 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-[#7F7F7F]">未检测到微信账户数据</p>
</div>
</div>
</div>
<!-- 未检测状态 -->
<div v-else class="bg-white rounded-2xl p-12 text-center">
<svg class="w-16 h-16 mx-auto text-[#7F7F7F] mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<p class="text-lg text-[#7F7F7F] mb-4">暂无检测结果</p>
<NuxtLink to="/"
class="inline-flex items-center px-6 py-3 bg-[#07C160] text-white rounded-lg font-medium hover:bg-[#06AD56] transition-colors">
返回首页开始检测
</NuxtLink>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useApi } from '~/composables/useApi'
const { detectWechat } = useApi()
const loading = ref(false)
const detectionResult = ref(null)
// 开始检测
const startDetection = async () => {
loading.value = true
try {
const result = await detectWechat()
detectionResult.value = result
} catch (err) {
console.error('检测过程中发生错误:', err)
detectionResult.value = {
status: 'error',
error: err.message || '检测过程中出现错误'
}
} finally {
loading.value = false
}
}
// 跳转到解密页面并传递账户信息
const goToDecrypt = (account) => {
// 将选中的账户信息存储到sessionStorage
if (process.client && typeof window !== 'undefined') {
sessionStorage.setItem('selectedAccount', JSON.stringify({
account_name: account.account_name,
data_dir: account.data_dir,
database_count: account.database_count,
databases: account.databases
}))
}
// 跳转到解密页面
navigateTo('/decrypt')
}
// 页面加载时自动检测
onMounted(() => {
startDetection()
// 调试:检查各元素高度
if (process.client) {
setTimeout(() => {
const mainContainer = document.querySelector('.min-h-screen')
const contentContainer = document.querySelector('.max-w-6xl')
console.log('=== 高度调试信息 ===')
console.log('视口高度:', window.innerHeight)
console.log('主容器高度:', mainContainer?.scrollHeight)
console.log('内容容器高度:', contentContainer?.scrollHeight)
console.log('body滚动高度:', document.body.scrollHeight)
console.log('documentElement滚动高度:', document.documentElement.scrollHeight)
// 检查是否有滚动条
const hasVerticalScrollbar = document.documentElement.scrollHeight > window.innerHeight
console.log('是否有垂直滚动条:', hasVerticalScrollbar)
}, 1000)
}
})
</script>
<style scoped>
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fade-in 0.8s ease-out;
}
/* 网格背景 */
.bg-grid-pattern {
background-image:
linear-gradient(rgba(7, 193, 96, 0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(7, 193, 96, 0.1) 1px, transparent 1px);
background-size: 50px 50px;
}
</style>

View File

@@ -0,0 +1,269 @@
<template>
<div class="min-h-screen flex items-center justify-center relative overflow-hidden">
<!-- 渐变背景 - 与首页保持一致 -->
<div class="absolute inset-0 bg-gradient-to-br from-[#F7F7F7] via-[#e6f7f0] to-[#F7F7F7]"></div>
<!-- 装饰性圆形渐变 -->
<div class="absolute top-1/4 -left-32 w-96 h-96 bg-gradient-to-br from-[#07C160] to-[#91D300] opacity-10 rounded-full blur-3xl"></div>
<div class="absolute bottom-1/4 -right-32 w-96 h-96 bg-gradient-to-br from-[#10AEEF] to-[#07C160] opacity-10 rounded-full blur-3xl"></div>
<!-- 返回按钮 -->
<NuxtLink to="/" class="absolute top-8 left-8 text-gray-600 hover:text-gray-900 transition-colors p-2 hover:bg-white/50 rounded-lg">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
</NuxtLink>
<!-- 主要内容区域 -->
<div class="relative z-10 text-center max-w-2xl mx-auto px-6">
<!-- 未检测状态 -->
<div v-if="!detectionResult && !loading" class="animate-fade-in">
<div class="mb-8">
<div class="inline-flex items-center justify-center w-24 h-24 bg-gradient-to-br from-green-400 to-green-600 rounded-2xl shadow-lg mb-6">
<svg class="w-14 h-14 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</div>
<h1 class="text-3xl font-bold text-gray-800 mb-3">微信检测</h1>
<p class="text-lg text-gray-600 mb-8">扫描系统中的微信安装信息和数据库文件</p>
</div>
<button
@click="startDetection"
class="group inline-flex items-center px-10 py-4 bg-gradient-to-r from-green-500 to-green-600 text-white rounded-xl text-lg font-semibold shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 transition-all duration-200"
>
<svg class="w-6 h-6 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
开始检测
</button>
</div>
<!-- 检测中状态 -->
<div v-if="loading" class="animate-fade-in">
<div class="mb-8">
<div class="relative inline-block mb-6">
<div class="w-24 h-24 bg-gradient-to-br from-green-400 to-green-600 rounded-2xl shadow-lg flex items-center justify-center">
<svg class="w-14 h-14 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</div>
<div class="absolute inset-0 w-24 h-24 rounded-2xl border-4 border-green-300 border-t-green-600 animate-spin"></div>
</div>
<h2 class="text-2xl font-bold text-gray-800 mb-3">正在检测中...</h2>
<p class="text-gray-600">请稍候正在扫描您的系统</p>
</div>
</div>
<!-- 检测结果 -->
<transition name="slide-fade">
<div v-if="detectionResult" class="animate-fade-in">
<div class="mb-8">
<div class="inline-flex items-center justify-center w-24 h-24 bg-gradient-to-br from-green-400 to-green-600 rounded-2xl shadow-lg mb-6">
<svg class="w-14 h-14 text-white" 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>
<h2 class="text-3xl font-bold text-gray-800 mb-3">检测完成</h2>
<p class="text-lg text-gray-600">发现 <span class="font-semibold text-green-600">{{ detectionResult.statistics.total_user_accounts }}</span> 个微信账户</p>
</div>
<!-- 结果卡片 -->
<div class="bg-white/80 backdrop-blur-sm rounded-2xl shadow-lg p-6 mb-6 text-left">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div class="text-center">
<div class="text-3xl font-bold text-green-600 mb-1">{{ detectionResult.statistics.total_user_accounts }}</div>
<div class="text-sm text-gray-600">账户数量</div>
</div>
<div class="text-center">
<div class="text-3xl font-bold text-blue-600 mb-1">{{ detectionResult.statistics.total_databases }}</div>
<div class="text-sm text-gray-600">数据库文件</div>
</div>
<div class="text-center">
<div class="text-xl font-semibold text-gray-800 mb-1">{{ detectionResult.data.wechat_version || '未知' }}</div>
<div class="text-sm text-gray-600">微信版本</div>
</div>
</div>
<!-- 账户列表 -->
<div v-if="detectionResult.data.user_accounts.length > 0" class="space-y-3">
<h3 class="text-sm font-semibold text-gray-700 mb-2">检测到的账户</h3>
<div v-for="(account, index) in detectionResult.data.user_accounts.slice(0, 3)" :key="index"
class="bg-gray-50 rounded-lg p-3 text-sm">
<div class="flex items-center justify-between">
<span class="font-medium text-gray-800">{{ account.wxid }}</span>
<button @click="copyText(account.db_storage_path)" class="text-gray-400 hover:text-green-600 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>
</button>
</div>
</div>
<p v-if="detectionResult.data.user_accounts.length > 3" class="text-xs text-gray-500 text-center">
还有 {{ detectionResult.data.user_accounts.length - 3 }} 个账户...
</p>
</div>
</div>
<!-- 操作按钮 -->
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<NuxtLink to="/decrypt"
class="group inline-flex items-center justify-center px-8 py-3 bg-gradient-to-r from-green-500 to-green-600 text-white rounded-xl text-base font-semibold shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 transition-all duration-200">
<svg class="w-5 h-5 mr-2.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
前往解密
<svg class="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" 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"/>
</svg>
</NuxtLink>
<button @click="resetDetection"
class="inline-flex items-center justify-center px-8 py-3 bg-white text-gray-700 border border-gray-300 rounded-xl text-base font-medium hover:bg-gray-50 transition-colors">
重新检测
</button>
</div>
</div>
</transition>
<!-- 错误提示 -->
<transition name="fade">
<div v-if="error" class="absolute bottom-8 left-1/2 transform -translate-x-1/2 bg-red-50 text-red-600 px-6 py-3 rounded-lg shadow-lg animate-shake">
<div class="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="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
{{ error }}
</div>
</div>
</transition>
</div>
<!-- 网格背景装饰 -->
<svg class="absolute inset-0 w-full h-full opacity-50" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="grid" width="60" height="60" patternUnits="userSpaceOnUse">
<path d="M 60 0 L 0 0 0 60" fill="none" stroke="rgba(0,0,0,0.03)" stroke-width="1"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />
</svg>
</div>
</template>
<style scoped>
/* 动画效果 */
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slide-fade {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes shake {
0%, 100% { transform: translateX(-50%); }
10%, 30%, 50%, 70%, 90% { transform: translateX(calc(-50% - 5px)); }
20%, 40%, 60%, 80% { transform: translateX(calc(-50% + 5px)); }
}
.animate-fade-in {
animation: fade-in 0.8s ease-out;
}
.slide-fade-enter-active {
animation: slide-fade 0.5s ease-out;
}
.animate-shake {
animation: shake 0.5s ease-in-out;
}
/* 页面过渡动画 */
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
</style>
<script setup>
import { ref, onMounted } from 'vue'
import { useApi } from '~/composables/useApi'
const { detectWechat } = useApi()
const loading = ref(false)
const error = ref('')
const detectionResult = ref(null)
// 页面加载时检查是否有存储的检测结果
onMounted(() => {
// 确保在客户端环境执行
if (process.client && typeof window !== 'undefined') {
const storedResult = sessionStorage.getItem('detectionResult')
if (storedResult) {
try {
detectionResult.value = JSON.parse(storedResult)
sessionStorage.removeItem('detectionResult') // 清除存储的结果
} catch (err) {
console.error('解析存储的检测结果失败:', err)
}
}
}
})
// 开始检测
const startDetection = async () => {
loading.value = true
error.value = ''
detectionResult.value = null
try {
const result = await detectWechat()
if (result.status === 'success') {
detectionResult.value = result
} else {
error.value = result.error || '检测失败,请重试'
}
} catch (err) {
error.value = err.message || '检测过程中发生错误'
} finally {
loading.value = false
}
}
// 重置检测
const resetDetection = () => {
detectionResult.value = null
error.value = ''
}
// 复制文本到剪贴板
const copyText = async (text) => {
if (!text) return
try {
await navigator.clipboard.writeText(text)
// 可以添加一个提示
} catch (err) {
console.error('复制失败:', err)
}
}
</script>

90
frontend/pages/index.vue Normal file
View File

@@ -0,0 +1,90 @@
<template>
<div class="min-h-screen flex items-center justify-center relative overflow-hidden">
<!-- 网格背景 -->
<div class="absolute inset-0 bg-grid-pattern opacity-5"></div>
<!-- 装饰元素 -->
<div class="absolute top-20 left-20 w-72 h-72 bg-[#07C160] opacity-5 rounded-full blur-3xl"></div>
<div class="absolute top-40 right-20 w-96 h-96 bg-[#10AEEF] opacity-5 rounded-full blur-3xl"></div>
<div class="absolute -bottom-8 left-40 w-80 h-80 bg-[#91D300] opacity-5 rounded-full blur-3xl"></div>
<!-- 主要内容区域 -->
<div class="relative z-10 text-center">
<!-- 标题部分 -->
<div class="mb-12 animate-fade-in">
<h1 class="text-5xl font-bold text-[#000000e6] mb-4">
<span class="bg-gradient-to-r from-[#07C160] to-[#10AEEF] bg-clip-text text-transparent">微信</span>
<span class="text-[#000000e6]">解密助手</span>
</h1>
<p class="text-xl text-[#7F7F7F] font-normal">轻松解锁你的聊天记录</p>
</div>
<!-- 主要按钮 -->
<div class="flex flex-col sm:flex-row gap-4 justify-center animate-slide-up">
<button @click="startDetection"
class="group inline-flex items-center px-12 py-4 bg-[#07C160] text-white rounded-lg text-lg font-medium hover:bg-[#06AD56] 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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<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>
</div>
</div>
</div>
</template>
<script setup>
// 开始检测并跳转到结果页面
const startDetection = async () => {
// 直接跳转到检测结果页面,让该页面处理检测
await navigateTo('/detection-result')
}
</script>
<style scoped>
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fade-in 0.8s ease-out;
}
.animate-slide-up {
animation: slide-up 0.8s ease-out 0.3s both;
}
/* 网格背景 */
.bg-grid-pattern {
background-image:
linear-gradient(rgba(7, 193, 96, 0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(7, 193, 96, 0.1) 1px, transparent 1px);
background-size: 50px 50px;
}
</style>

View File

@@ -0,0 +1,26 @@
// 客户端插件检查API连接状态
export default defineNuxtPlugin(async (nuxtApp) => {
const { healthCheck } = useApi()
const appStore = useAppStore()
// 检查API连接
const checkApiConnection = async () => {
try {
const result = await healthCheck()
if (result.status === 'healthy') {
appStore.setApiStatus('connected', '已连接到后端API')
} else {
appStore.setApiStatus('error', 'API响应异常')
}
} catch (error) {
appStore.setApiStatus('error', '无法连接到后端API请确保后端服务已启动')
console.error('API连接失败:', error)
}
}
// 初始检查
await checkApiConnection()
// 定期检查每30秒
setInterval(checkApiConnection, 30000)
})

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

75
frontend/stores/app.js Normal file
View File

@@ -0,0 +1,75 @@
import { defineStore } from 'pinia'
export const useAppStore = defineStore('app', {
state: () => ({
// API连接状态
apiStatus: 'unknown', // unknown, connected, error
apiMessage: '',
// 最近的检测结果
lastDetectionResult: null,
// 全局加载状态
globalLoading: false,
// 全局错误信息
globalError: null
}),
actions: {
// 设置API状态
setApiStatus(status, message = '') {
this.apiStatus = status
this.apiMessage = message
},
// 保存检测结果
saveDetectionResult(result) {
this.lastDetectionResult = result
},
// 设置全局加载状态
setGlobalLoading(loading) {
this.globalLoading = loading
},
// 设置全局错误
setGlobalError(error) {
this.globalError = error
// 3秒后自动清除错误
if (error) {
setTimeout(() => {
this.globalError = null
}, 3000)
}
},
// 清除全局错误
clearGlobalError() {
this.globalError = null
}
},
getters: {
// 是否已连接到API
isApiConnected: (state) => state.apiStatus === 'connected',
// 是否有检测结果
hasDetectionResult: (state) => state.lastDetectionResult !== null,
// 获取可用的数据库路径列表
availableDbPaths: (state) => {
if (!state.lastDetectionResult || !state.lastDetectionResult.data) {
return []
}
const accounts = state.lastDetectionResult.data.user_accounts || []
return accounts
.filter(account => account.db_storage_path)
.map(account => ({
wxid: account.wxid,
path: account.db_storage_path
}))
}
}
})

View File

@@ -0,0 +1,23 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./components/**/*.{js,vue,ts}",
"./layouts/**/*.vue",
"./pages/**/*.vue",
"./plugins/**/*.{js,ts}",
"./app.vue",
"./error.vue",
"./nuxt.config.{js,ts}",
],
theme: {
extend: {
colors: {
wechat: {
green: '#07c160',
'green-hover': '#06a050',
}
}
},
},
plugins: [],
}

18
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,18 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"files": [],
"references": [
{
"path": "./.nuxt/tsconfig.app.json"
},
{
"path": "./.nuxt/tsconfig.server.json"
},
{
"path": "./.nuxt/tsconfig.shared.json"
},
{
"path": "./.nuxt/tsconfig.node.json"
}
]
}

View File

@@ -349,7 +349,8 @@ async def decrypt_databases(request: DecryptRequest):
"output_directory": results["output_directory"],
"message": results["message"],
"processed_files": results["processed_files"],
"failed_files": results["failed_files"]
"failed_files": results["failed_files"],
"account_results": results.get("account_results", {})
}
except Exception as e: