feat: 恢复年度总结按钮,并将导入功能独立为单独页面

This commit is contained in:
H3CoF6
2026-04-08 07:05:06 +08:00
Unverified
parent ad6031651b
commit 96685490ac
2 changed files with 219 additions and 133 deletions
+204
View File
@@ -0,0 +1,204 @@
<template>
<div class="import-page min-h-screen flex items-center justify-center py-8">
<div class="max-w-2xl mx-auto px-6 w-full">
<div class="bg-white rounded-3xl border border-[#EDEDED] shadow-sm overflow-hidden">
<div class="p-8 md:p-12">
<!-- 标题部分 -->
<div class="flex items-center mb-8">
<div class="w-14 h-14 bg-[#91D300] rounded-2xl flex items-center justify-center mr-5 shadow-sm">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
</svg>
</div>
<div>
<h2 class="text-2xl font-bold text-[#000000e6]">数据导入</h2>
<p class="text-[#7F7F7F] mt-1">导入已解密的数据库备份目录</p>
</div>
</div>
<!-- 导入说明 -->
<div class="bg-blue-50 border border-blue-100 rounded-2xl p-6 mb-8">
<h3 class="text-blue-800 font-bold mb-3 flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
标准目录结构要求
</h3>
<ul class="text-sm text-blue-700 space-y-2 list-disc list-inside opacity-90">
<li><strong>databases/</strong> 目录存放扁平化的 .db 文件</li>
<li><strong>account.json</strong> 文件包含昵称wxid等信息</li>
<li><strong>resource/</strong> 目录存放解密后的资源文件可选</li>
</ul>
</div>
<!-- 初始状态选择目录 -->
<div v-if="!importPreview && !importError" class="flex flex-col items-center justify-center py-12 border-2 border-dashed border-[#EDEDED] rounded-3xl hover:border-[#91D300] transition-colors cursor-pointer group" @click="handlePickDirectory">
<div class="w-20 h-20 bg-gray-50 rounded-full flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300">
<svg class="w-10 h-10 text-gray-400 group-hover:text-[#91D300]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
</svg>
</div>
<div class="text-[#000000e6] font-medium text-lg">点击选择备份目录</div>
<p class="text-[#7F7F7F] text-sm mt-2">支持原生目录选择器</p>
</div>
<!-- 预览状态显示账号信息 -->
<div v-if="importPreview" class="animate-fade-in">
<div class="flex flex-col items-center py-8 bg-[#FBFBFB] rounded-3xl border border-[#EDEDED] mb-8">
<div class="w-28 h-28 rounded-full overflow-hidden border-4 border-white shadow-md mb-5">
<img :src="importPreview.avatar_url || '/Contact.png'" class="w-full h-full object-cover" alt="头像">
</div>
<div class="text-center">
<div class="text-xl font-bold text-gray-900">{{ importPreview.nick }}</div>
<div class="text-sm text-gray-500 font-mono mt-1 bg-white px-3 py-1 rounded-full border border-[#EDEDED] inline-block">{{ importPreview.username }}</div>
</div>
<div class="mt-8 flex gap-6">
<div class="flex items-center text-sm text-gray-600">
<div class="w-2 h-2 rounded-full bg-[#07C160] mr-2"></div>
数据库已就绪
</div>
<div class="flex items-center text-sm text-gray-600">
<div class="w-2 h-2 rounded-full mr-2" :class="importPreview.has_resource ? 'bg-[#07C160]' : 'bg-gray-300'"></div>
资源文件{{ importPreview.has_resource ? '已发现' : '未发现' }}
</div>
</div>
</div>
<div class="flex gap-4">
<button @click="resetImport"
class="flex-1 px-8 py-4 border border-[#EDEDED] text-gray-600 rounded-2xl font-bold hover:bg-gray-50 transition-all">
重新选择
</button>
<button @click="confirmImport" :disabled="importing"
class="flex-[2] px-8 py-4 bg-[#91D300] text-white rounded-2xl font-bold hover:bg-[#82BD00] shadow-lg shadow-[#91D300]/20 disabled:opacity-50 transition-all flex items-center justify-center transform hover:scale-[1.02] active:scale-[0.98]">
<svg v-if="importing" class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" fill="none" 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>
<span v-if="!importing">确认导入此账号</span>
<span v-else>正在导入数据...</span>
</button>
</div>
</div>
<!-- 错误状态 -->
<div v-if="importError" class="animate-fade-in">
<div class="p-6 bg-red-50 border border-red-100 rounded-2xl flex items-start mb-8">
<svg class="w-6 h-6 text-red-500 mr-3 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<div>
<p class="font-bold text-red-800 mb-1">目录校验失败</p>
<p class="text-sm text-red-600">{{ importError }}</p>
</div>
</div>
<button @click="resetImport"
class="w-full px-8 py-4 bg-white border border-[#EDEDED] text-[#07C160] rounded-2xl font-bold hover:bg-gray-50 transition-all flex items-center justify-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
重新选择目录
</button>
</div>
<!-- 返回首页 -->
<div class="mt-12 text-center">
<NuxtLink to="/" class="text-[#7F7F7F] hover:text-[#07C160] text-sm transition-colors inline-flex items-center">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
返回首页
</NuxtLink>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useApi } from '~/composables/useApi'
const { importDecryptedPreview, importDecrypted } = useApi()
const importing = ref(false)
const importPreview = ref(null)
const importError = ref('')
const selectedImportPath = ref('')
const isDesktopShell = () => {
if (!process.client || typeof window === 'undefined') return false
return !!window.wechatDesktop?.__brand
}
const resetImport = () => {
importPreview.value = null
importError.value = ''
selectedImportPath.value = ''
}
const handlePickDirectory = async () => {
let path = ''
if (isDesktopShell()) {
try {
const res = await window.wechatDesktop.chooseDirectory({
title: '选择解密数据所在目录'
})
if (!res || res.canceled || !res.filePaths?.length) return
path = res.filePaths[0]
} catch (e) {
console.error('选择目录失败:', e)
return
}
} else {
path = window.prompt('请输入已解密目录的绝对路径:')
if (!path) return
}
selectedImportPath.value = path
importError.value = ''
importPreview.value = null
try {
const res = await importDecryptedPreview({ import_path: path })
importPreview.value = res
} catch (e) {
importError.value = e.message || '目录格式不正确,请确保包含 databases 目录和 account.json'
}
}
const confirmImport = async () => {
if (!selectedImportPath.value) return
importing.value = true
importError.value = ''
try {
const res = await importDecrypted({ import_path: selectedImportPath.value })
if (res.status === 'success') {
await navigateTo('/chat')
} else {
importError.value = res.message || '导入失败'
}
} catch (e) {
importError.value = e.message || '导入过程中发生错误'
} finally {
importing.value = false
}
}
</script>
<style scoped>
.animate-fade-in {
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
+15 -133
View File
@@ -42,13 +42,13 @@
<span>直接解密</span>
</NuxtLink>
<button @click="handleImportClick"
<NuxtLink to="/import"
class="group inline-flex items-center px-12 py-4 bg-white text-[#91D300] border border-[#91D300] rounded-lg text-lg font-medium hover:bg-[#F7F7F7] transform hover:scale-105 transition-all duration-200">
<svg class="w-6 h-6 mr-3 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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
</svg>
<span>数据导入</span>
</button>
</NuxtLink>
<NuxtLink to="/chat"
class="group inline-flex items-center px-12 py-4 bg-white text-[#10AEEF] border border-[#10AEEF] rounded-lg text-lg font-medium hover:bg-[#F7F7F7] transform hover:scale-105 transition-all duration-200">
@@ -57,85 +57,28 @@
</svg>
<span>聊天预览</span>
</NuxtLink>
<NuxtLink to="/wrapped"
class="group inline-flex items-center px-12 py-4 bg-white text-[#B37800] border border-[#F2AA00] 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 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<rect x="4" y="4" width="16" height="16" rx="2" stroke-width="2.5" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M8 16v-5" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 16v-8" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M16 16v-3" />
</svg>
<span>年度总结</span>
</NuxtLink>
</div>
</div>
<!-- 导入预览对话框 -->
<transition name="fade">
<div v-if="showImportModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
<div class="bg-white rounded-2xl w-full max-w-md shadow-2xl overflow-hidden animate-slide-up">
<div class="p-8">
<h3 class="text-xl font-bold text-gray-900 mb-6 flex items-center">
<svg class="w-6 h-6 mr-2 text-[#91D300]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
确认导入账号
</h3>
<div v-if="importPreview" class="flex flex-col items-center mb-8">
<div class="w-24 h-24 rounded-full overflow-hidden border-4 border-[#F7F7F7] shadow-sm mb-4">
<img :src="importPreview.avatar_url || '/Contact.png'" class="w-full h-full object-cover" alt="头像">
</div>
<div class="text-center">
<div class="text-lg font-bold text-gray-900">{{ importPreview.nick }}</div>
<div class="text-sm text-gray-500 font-mono mt-1">{{ importPreview.username }}</div>
</div>
<div class="mt-6 w-full bg-gray-50 rounded-xl p-4 text-sm text-gray-600 space-y-2">
<div class="flex justify-between items-center">
<span>包含数据库</span>
<span class="text-[#07C160] font-medium"></span>
</div>
<div class="flex justify-between items-center">
<span>包含资源文件</span>
<span :class="importPreview.has_resource ? 'text-[#07C160]' : 'text-gray-400'" class="font-medium">
{{ importPreview.has_resource ? '是' : '否' }}
</span>
</div>
</div>
</div>
<div v-if="importError" class="mb-6 p-4 bg-red-50 border border-red-100 rounded-xl flex items-start">
<svg class="w-5 h-5 text-red-500 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>
<span class="text-sm text-red-600">{{ importError }}</span>
</div>
<div class="flex gap-4">
<button @click="showImportModal = false"
class="flex-1 px-6 py-3 border border-gray-200 text-gray-600 rounded-xl font-medium hover:bg-gray-50 transition-colors">
取消
</button>
<button @click="confirmImport" :disabled="importing"
class="flex-1 px-6 py-3 bg-[#91D300] text-white rounded-xl font-medium hover:bg-[#82BD00] disabled:opacity-50 transition-colors flex items-center justify-center">
<svg v-if="importing" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" 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>
确认导入
</button>
</div>
</div>
</div>
</div>
</transition>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { onMounted } from 'vue'
import { useApi } from '~/composables/useApi'
import { DESKTOP_SETTING_DEFAULT_TO_CHAT_KEY, readLocalBoolSetting } from '~/lib/desktop-settings'
const { listChatAccounts, importDecryptedPreview, importDecrypted } = useApi()
// 导入相关
const showImportModal = ref(false)
const importing = ref(false)
const importPreview = ref(null)
const importError = ref('')
const selectedImportPath = ref('')
const { listChatAccounts } = useApi()
onMounted(async () => {
if (!process.client || typeof window === 'undefined') return
@@ -152,67 +95,6 @@ onMounted(async () => {
} catch {}
})
const isDesktopShell = () => {
if (!process.client || typeof window === 'undefined') return false
return !!window.wechatDesktop?.__brand
}
// 导入点击
const handleImportClick = async () => {
let path = ''
if (isDesktopShell()) {
try {
const res = await window.wechatDesktop.chooseDirectory({
title: '选择解密数据所在目录'
})
if (!res || res.canceled || !res.filePaths?.length) return
path = res.filePaths[0]
} catch (e) {
console.error('选择目录失败:', e)
return
}
} else {
// 网页版演示,弹出提示让输入(通常在本地开发使用)
path = window.prompt('请输入已解密目录的绝对路径:')
if (!path) return
}
selectedImportPath.value = path
importError.value = ''
importPreview.value = null
showImportModal.value = true
try {
const res = await importDecryptedPreview({ import_path: path })
importPreview.value = res
} catch (e) {
importError.value = e.message || '目录格式不正确,请确保包含 databases 目录和 account.json'
}
}
// 确认导入
const confirmImport = async () => {
if (!selectedImportPath.value) return
importing.value = true
importError.value = ''
try {
const res = await importDecrypted({ import_path: selectedImportPath.value })
if (res.status === 'success') {
// 导入成功后,可以跳转到聊天界面
await navigateTo('/chat')
} else {
importError.value = res.message || '导入失败'
}
} catch (e) {
importError.value = e.message || '导入过程中发生错误'
} finally {
importing.value = false
}
}
// 开始检测并跳转到结果页面
const startDetection = async () => {
// 直接跳转到检测结果页面,让该页面处理检测