mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-02-02 22:10:50 +08:00
feat(frontend): 添加前端页面
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,3 +12,4 @@ wheels/
|
||||
/.idea
|
||||
/.history/
|
||||
/.augment/
|
||||
/CLAUDE.md
|
||||
|
||||
35
README.md
35
README.md
@@ -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
24
frontend/.gitignore
vendored
Normal 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
18
frontend/app.vue
Normal 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>
|
||||
163
frontend/assets/css/tailwind.css
Normal file
163
frontend/assets/css/tailwind.css
Normal 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;
|
||||
}
|
||||
}
|
||||
21
frontend/components/ApiStatus.vue
Normal file
21
frontend/components/ApiStatus.vue
Normal 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>
|
||||
52
frontend/composables/useApi.js
Normal file
52
frontend/composables/useApi.js
Normal 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
50
frontend/nuxt.config.ts
Normal 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
13525
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
frontend/package.json
Normal file
20
frontend/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
174
frontend/pages/decrypt-result.vue
Normal file
174
frontend/pages/decrypt-result.vue
Normal 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
252
frontend/pages/decrypt.vue
Normal 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>
|
||||
268
frontend/pages/detection-result.vue
Normal file
268
frontend/pages/detection-result.vue
Normal 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>
|
||||
269
frontend/pages/detection.vue
Normal file
269
frontend/pages/detection.vue
Normal 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
90
frontend/pages/index.vue
Normal 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>
|
||||
26
frontend/plugins/api-check.client.js
Normal file
26
frontend/plugins/api-check.client.js
Normal 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
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
75
frontend/stores/app.js
Normal 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
|
||||
}))
|
||||
}
|
||||
}
|
||||
})
|
||||
23
frontend/tailwind.config.js
Normal file
23
frontend/tailwind.config.js
Normal 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
18
frontend/tsconfig.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user