feat(chat): 前端聊天页支持消息搜索与筛选

- 新增搜索侧边栏:会话内/全局搜索、时间范围、发送者与类型筛选

- 支持搜索结果高亮与上下文定位

- 对接后端索引构建状态与错误提示
This commit is contained in:
2977094657
2025-12-25 20:28:12 +08:00
parent fa08937ebd
commit ab91e5bb6e
5 changed files with 2755 additions and 324 deletions

View File

@@ -188,4 +188,735 @@
.fade-enter-to { .fade-enter-to {
@apply opacity-100 transform scale-100; @apply opacity-100 transform scale-100;
} }
}
/* 现代化搜索面板样式 */
.search-panel-container {
@apply border-b border-gray-200;
background: linear-gradient(to bottom, #f8f9fa, #f0f1f2);
}
.search-panel {
@apply px-5 py-4;
}
/* 搜索输入行 */
.search-input-row {
@apply flex items-center gap-3;
}
.search-input-wrapper {
@apply flex-1 flex items-center bg-white border border-gray-200 rounded-lg overflow-hidden transition-all duration-200;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.search-input-wrapper:hover {
@apply border-gray-300;
}
.search-input-focused {
@apply border-[#03C160] ring-2 ring-[#03C160]/20;
}
.search-input-icon {
@apply w-4 h-4 text-gray-400 ml-3 flex-shrink-0;
}
.search-input {
@apply flex-1 px-3 py-2.5 text-sm bg-transparent border-none outline-none;
}
.search-input::placeholder {
@apply text-gray-400;
}
.search-clear-btn {
@apply p-2 text-gray-400 hover:text-gray-600 transition-colors;
}
.search-btn-primary {
@apply px-4 py-2.5 text-sm font-medium text-white bg-[#03C160] rounded-lg hover:bg-[#02a650] transition-all duration-200 flex items-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed;
box-shadow: 0 2px 4px rgba(3, 193, 96, 0.2);
}
.search-btn-primary:hover:not(:disabled) {
box-shadow: 0 4px 8px rgba(3, 193, 96, 0.3);
transform: translateY(-1px);
}
.search-btn-close {
@apply p-2.5 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors;
}
/* 搜索控制行 */
.search-controls-row {
@apply flex items-center gap-3 mt-3 flex-wrap;
}
/* 搜索范围切换器 */
.search-scope-switcher {
@apply flex items-center bg-gray-100 rounded-lg p-0.5;
}
.scope-btn {
@apply flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-600 rounded-md transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed;
}
.scope-btn:hover:not(:disabled) {
@apply text-gray-800;
}
.scope-btn-active {
@apply bg-white text-[#03C160] shadow-sm;
}
/* 快捷过滤标签 */
.quick-filter-tags {
@apply flex items-center gap-1.5 flex-wrap;
}
.filter-tag {
@apply flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium text-gray-600 bg-white border border-gray-200 rounded-full transition-all duration-200 hover:border-gray-300 hover:bg-gray-50;
}
.filter-tag-icon {
@apply text-sm;
}
.filter-tag-active {
@apply bg-[#03C160]/10 border-[#03C160] text-[#03C160];
}
.filter-tag-active:hover {
@apply bg-[#03C160]/15 border-[#03C160];
}
/* 高级过滤器切换 */
.advanced-filter-toggle {
@apply flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-600 bg-white border border-gray-200 rounded-lg transition-all duration-200 hover:border-gray-300 hover:bg-gray-50 ml-auto;
}
.advanced-filter-toggle-active {
@apply bg-gray-100 border-gray-300;
}
/* 高级过滤器面板 */
.advanced-filters-panel {
@apply overflow-hidden transition-all duration-300 ease-in-out;
max-height: 0;
opacity: 0;
}
.advanced-filters-expanded {
max-height: 200px;
opacity: 1;
@apply mt-3;
}
.advanced-filters-content {
@apply flex items-center gap-4 p-3 bg-white rounded-lg border border-gray-200 flex-wrap;
}
.filter-group {
@apply flex items-center gap-2;
}
.filter-label {
@apply text-xs font-medium text-gray-600;
}
.filter-select {
@apply text-xs px-3 py-1.5 rounded-md border border-gray-200 bg-white focus:outline-none focus:ring-2 focus:ring-[#03C160]/20 focus:border-[#03C160] transition-all;
}
.filter-checkbox {
@apply flex items-center gap-2 text-xs cursor-pointer;
}
.filter-checkbox-input {
@apply w-4 h-4 text-[#03C160] border-gray-300 rounded focus:ring-[#03C160]/20;
}
.filter-checkbox-label {
@apply font-medium text-gray-700;
}
.filter-checkbox-hint {
@apply text-gray-400;
}
/* 搜索历史面板 */
.search-history-panel {
@apply mt-3 p-3 bg-white rounded-lg border border-gray-200;
}
.search-history-header {
@apply flex items-center justify-between mb-2;
}
.search-history-title {
@apply text-xs font-medium text-gray-600;
}
.search-history-clear {
@apply text-xs text-gray-400 hover:text-gray-600 transition-colors;
}
.search-history-list {
@apply flex flex-wrap gap-2;
}
.search-history-item {
@apply flex items-center gap-1.5 px-2.5 py-1.5 text-xs text-gray-600 bg-gray-50 rounded-full hover:bg-gray-100 transition-colors;
}
/* 搜索状态栏 */
.search-status-bar {
@apply mt-3 text-xs;
}
.search-status-error {
@apply flex items-center gap-2 text-red-600 bg-red-50 px-3 py-2 rounded-lg;
}
.search-status-info {
@apply text-gray-600;
}
.search-status-count {
@apply text-gray-800;
}
.search-status-count strong {
@apply text-[#03C160] font-semibold;
}
.search-status-detail {
@apply text-gray-500;
}
.search-status-hint {
@apply flex items-center gap-2 text-gray-400;
}
/* 搜索结果容器 */
.search-results-container {
@apply bg-white border-b border-gray-200;
max-height: 320px;
}
.search-results-list {
@apply overflow-y-auto;
max-height: 260px;
}
/* 搜索结果卡片 */
.search-result-card {
@apply px-5 py-3 border-b border-gray-100 cursor-pointer transition-all duration-150;
border-left: 3px solid transparent;
}
.search-result-card:hover {
@apply bg-gray-50;
border-left-color: #e5e7eb;
}
.search-result-card-selected {
@apply bg-[#03C160]/5;
border-left-color: #03C160;
}
.search-result-card-selected:hover {
@apply bg-[#03C160]/10;
border-left-color: #03C160;
}
.search-result-meta {
@apply flex items-center gap-2 text-[11px] text-gray-500 mb-1;
}
.search-result-contact {
@apply font-medium text-gray-700;
}
.search-result-contact::after {
content: '·';
@apply ml-2;
}
.search-result-time {
@apply text-gray-400;
}
.search-result-sender {
@apply text-gray-500;
}
.search-result-sender::before {
content: '·';
@apply mr-2;
}
.search-result-content {
@apply text-sm text-gray-800 truncate leading-relaxed;
}
/* 搜索高亮 */
.search-highlight {
@apply bg-[#03C160]/20 text-[#03C160] px-0.5 rounded font-medium;
}
/* 搜索结果底部 */
.search-results-footer {
@apply px-5 py-3 flex items-center justify-between border-t border-gray-100 bg-gray-50/50;
}
.search-load-more-btn {
@apply flex items-center gap-2 text-xs px-3 py-1.5 rounded-md border border-gray-200 bg-white hover:bg-gray-50 text-gray-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed;
}
.search-results-hint {
@apply text-xs text-gray-400;
}
.search-results-hint kbd {
@apply px-1.5 py-0.5 bg-gray-100 border border-gray-200 rounded text-[10px] font-mono;
}
/* 空状态 */
.search-empty-state {
@apply py-8 px-5 text-center bg-white border-b border-gray-200;
}
.search-empty-icon {
@apply w-16 h-16 mx-auto text-gray-300 mb-4;
}
.search-empty-title {
@apply text-base font-medium text-gray-600 mb-3;
}
.search-empty-tips {
@apply text-sm text-gray-500 text-left max-w-xs mx-auto;
}
.search-empty-tips p {
@apply mb-2;
}
.search-empty-tips ul {
@apply list-disc list-inside space-y-1 text-gray-400;
}
/* 联系人搜索栏样式 */
.contact-search-wrapper {
@apply flex items-center bg-[#EAEAEA] rounded-lg overflow-hidden transition-all duration-200;
}
.contact-search-wrapper:focus-within {
@apply ring-2 ring-[#03C160]/20 bg-white;
}
.contact-search-icon {
@apply w-4 h-4 text-gray-400 ml-2.5 flex-shrink-0;
}
.contact-search-input {
@apply flex-1 px-2 py-2 text-sm bg-transparent border-none outline-none;
}
.contact-search-input::placeholder {
@apply text-gray-400;
}
.contact-search-clear {
@apply p-2 text-gray-400 hover:text-gray-600 transition-colors;
}
.account-select {
@apply text-xs px-2 py-2 rounded-lg border border-gray-200 bg-[#EAEAEA] focus:outline-none focus:ring-2 focus:ring-[#03C160]/20 focus:border-[#03C160] transition-all;
}
/* 骨架屏动画 */
.skeleton-pulse {
animation: skeleton-pulse 1.5s ease-in-out infinite;
}
@keyframes skeleton-pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
/* 聊天头部样式 */
.chat-header {
@apply h-14 px-5 flex items-center border-b border-gray-200;
background-color: #EDEDED;
}
.header-btn {
@apply flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-lg bg-white border border-gray-200 text-gray-700 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed;
}
.header-btn:hover:not(:disabled) {
@apply bg-gray-50 border-gray-300;
}
.header-btn:active:not(:disabled) {
@apply bg-gray-100;
}
.header-btn-icon {
@apply w-8 h-8 flex items-center justify-center rounded-lg bg-white border border-gray-200 text-gray-600 transition-all duration-200;
}
.header-btn-icon:hover {
@apply bg-gray-50 border-gray-300 text-gray-800;
}
.header-btn-icon-active {
@apply bg-[#03C160]/10 border-[#03C160] text-[#03C160];
}
.header-btn-icon-active:hover {
@apply bg-[#03C160]/15;
}
/* 搜索侧边栏样式 */
.search-sidebar {
@apply w-[420px] h-full flex flex-col bg-white border-l border-gray-200 flex-shrink-0;
}
.search-sidebar-header {
@apply flex items-center justify-between px-4 py-3 border-b border-gray-200 bg-gray-50;
}
.search-sidebar-title {
@apply flex items-center gap-2 text-sm font-medium text-gray-800;
}
.search-sidebar-close {
@apply p-1.5 text-gray-500 hover:text-gray-700 hover:bg-gray-200 rounded-md transition-colors;
}
.search-sidebar-input-section {
@apply px-3 py-3 border-b border-gray-100;
}
/* 整合搜索框样式 */
.search-input-combined {
@apply flex items-center bg-white border-2 border-gray-200 rounded-lg overflow-hidden transition-all duration-200;
}
.search-input-combined:hover {
@apply border-gray-300;
}
.search-input-combined-focused {
@apply border-[#03C160] ring-2 ring-[#03C160]/20;
}
.search-scope-inline {
@apply flex items-center px-2 border-r border-gray-200 bg-gray-50;
}
.scope-inline-btn {
@apply text-[11px] font-medium text-gray-400 hover:text-gray-600 transition-colors px-1 py-2.5 disabled:opacity-40 disabled:cursor-not-allowed;
}
.scope-inline-btn-active {
@apply text-[#03C160] font-semibold;
}
.scope-inline-divider {
@apply text-gray-300 text-[10px];
}
.search-input-inline {
@apply flex-1 min-w-0 px-3 py-2.5 text-sm bg-transparent border-none outline-none;
}
.search-input-inline::placeholder {
@apply text-gray-400;
}
.search-clear-inline {
@apply p-2 text-gray-400 hover:text-gray-600 transition-colors;
}
.search-btn-inline {
@apply flex items-center justify-center w-10 h-10 bg-[#03C160] text-white hover:bg-[#02a650] transition-colors disabled:opacity-60 disabled:cursor-not-allowed flex-shrink-0;
}
/* 筛选条件行 */
.search-filters-row {
@apply flex items-center gap-2 mt-2;
}
.search-session-type-row {
@apply flex items-center gap-1 mt-2;
}
.search-session-type-btn {
@apply flex-1 text-[11px] font-medium px-2 py-1.5 rounded-md border border-gray-200 bg-gray-50 text-gray-500 hover:bg-gray-100 hover:text-gray-700 transition-colors;
}
.search-session-type-btn-active {
@apply bg-[#03C160]/10 border-[#03C160] text-[#03C160] font-semibold;
}
.search-filter-select {
@apply flex-1 text-xs px-2 py-1.5 bg-gray-50 border border-gray-200 rounded-md outline-none cursor-pointer transition-all hover:border-gray-300 focus:border-[#03C160] focus:ring-1 focus:ring-[#03C160]/20;
}
.search-filter-select-time {
@apply flex-none w-28;
}
.search-filter-input {
@apply flex-1 text-xs px-2 py-1.5 bg-gray-50 border border-gray-200 rounded-md outline-none transition-all hover:border-gray-300 focus:border-[#03C160] focus:ring-1 focus:ring-[#03C160]/20;
}
.search-filter-input::placeholder {
@apply text-gray-400;
}
/* 自定义日期行 */
.search-custom-date-row {
@apply flex items-center gap-2 mt-2;
}
.search-date-input {
@apply flex-1 text-xs px-2 py-1.5 bg-gray-50 border border-gray-200 rounded-md outline-none transition-all hover:border-gray-300 focus:border-[#03C160] focus:ring-1 focus:ring-[#03C160]/20;
}
.search-date-separator {
@apply text-xs text-gray-400;
}
.search-sidebar-scope {
@apply px-4 py-3 border-b border-gray-100;
}
.search-sidebar-filters {
@apply px-4 py-3 border-b border-gray-100;
}
.quick-filter-tags-vertical {
@apply flex flex-wrap gap-1.5 mt-2;
}
.filter-tag-vertical {
@apply flex items-center gap-1 px-2 py-1 text-xs font-medium text-gray-600 bg-gray-50 border border-gray-200 rounded-md transition-all duration-200 hover:border-gray-300 hover:bg-gray-100;
}
.search-sidebar-advanced {
@apply px-4 py-3 border-b border-gray-100;
}
.sidebar-section-toggle {
@apply flex items-center justify-between w-full text-left;
}
.sidebar-section-title {
@apply text-xs font-medium text-gray-500 uppercase tracking-wide;
}
.sidebar-advanced-content {
@apply overflow-hidden transition-all duration-300 ease-in-out;
max-height: 0;
opacity: 0;
}
.sidebar-advanced-expanded {
max-height: 200px;
opacity: 1;
@apply mt-3;
}
.sidebar-filter-group {
@apply mb-3;
}
.sidebar-filter-label {
@apply block text-xs text-gray-600 mb-1;
}
.sidebar-filter-select {
@apply w-full text-xs px-2.5 py-1.5 rounded-md border border-gray-200 bg-white focus:outline-none focus:ring-2 focus:ring-[#03C160]/20 focus:border-[#03C160] transition-all;
}
.sidebar-checkbox {
@apply flex items-center gap-2 text-xs text-gray-600 cursor-pointer;
}
.sidebar-checkbox input {
@apply w-3.5 h-3.5 text-[#03C160] border-gray-300 rounded focus:ring-[#03C160]/20;
}
.search-sidebar-history {
@apply px-4 py-3 border-b border-gray-100;
}
.sidebar-section-header {
@apply flex items-center justify-between mb-2;
}
.sidebar-clear-btn {
@apply text-xs text-gray-400 hover:text-gray-600 transition-colors;
}
.sidebar-index-btn {
@apply px-2 py-1 text-[10px] font-medium text-gray-600 bg-gray-50 border border-gray-200 rounded-md hover:bg-gray-100 hover:border-gray-300 transition-colors disabled:opacity-60 disabled:cursor-not-allowed whitespace-nowrap;
}
.sidebar-history-list {
@apply space-y-1;
}
.sidebar-history-item {
@apply flex items-center gap-2 w-full px-2 py-1.5 text-xs text-gray-600 rounded-md hover:bg-gray-50 transition-colors text-left;
}
.search-sidebar-status {
@apply px-4 py-2 text-xs border-b border-gray-100;
}
.sidebar-status-error {
@apply text-red-600;
}
.sidebar-status-info {
@apply text-gray-600;
}
.sidebar-status-info strong {
@apply text-[#03C160] font-semibold;
}
.sidebar-status-detail {
@apply text-gray-400;
}
.search-sidebar-results {
@apply flex-1 overflow-y-auto min-h-0;
}
.sidebar-results-list {
@apply divide-y divide-gray-100;
}
.sidebar-result-card {
@apply px-4 py-3 cursor-pointer transition-all duration-150 hover:bg-gray-50;
border-left: 3px solid transparent;
}
.sidebar-result-row {
@apply flex items-start gap-3;
}
.sidebar-result-avatar {
@apply w-9 h-9 rounded-md overflow-hidden bg-gray-300 flex-shrink-0;
}
.sidebar-result-avatar-fallback {
@apply w-full h-full flex items-center justify-center text-white text-[10px] font-bold bg-gray-600;
}
.sidebar-result-body {
@apply min-w-0 flex-1;
}
.sidebar-result-card-selected {
@apply bg-[#03C160]/5;
border-left-color: #03C160;
}
.sidebar-result-card-selected:hover {
@apply bg-[#03C160]/10;
}
.sidebar-result-header {
@apply flex items-center gap-2 text-[10px] text-gray-400 mb-0.5;
}
.sidebar-result-contact {
@apply font-medium text-gray-600;
}
.sidebar-result-time {
@apply text-gray-400;
}
.sidebar-result-sender {
@apply text-[11px] text-gray-500 mb-1;
}
.sidebar-result-content {
@apply text-xs text-gray-700 line-clamp-2 leading-relaxed;
}
.sidebar-load-more {
@apply px-4 py-3;
}
.sidebar-load-more-btn {
@apply w-full text-xs px-3 py-2 rounded-md border border-gray-200 bg-white hover:bg-gray-50 text-gray-600 transition-colors disabled:opacity-50;
}
.sidebar-empty-state {
@apply flex flex-col items-center justify-center py-12 px-4 text-center;
}
.sidebar-empty-icon {
@apply w-12 h-12 text-gray-300 mb-3;
}
.sidebar-empty-text {
@apply text-sm font-medium text-gray-500 mb-1;
}
.sidebar-empty-hint {
@apply text-xs text-gray-400;
}
.sidebar-initial-state {
@apply flex flex-col items-center justify-center py-12 px-4 text-center;
}
.sidebar-initial-icon {
@apply w-16 h-16 text-gray-200 mb-4;
}
.sidebar-initial-text {
@apply text-sm text-gray-500 mb-2;
}
.sidebar-initial-hint {
@apply text-xs text-gray-400;
}
.sidebar-initial-hint kbd {
@apply px-1.5 py-0.5 bg-gray-100 border border-gray-200 rounded text-[10px] font-mono;
}
/* 侧边栏滑入动画 */
.sidebar-slide-enter-active,
.sidebar-slide-leave-active {
transition: all 0.3s ease;
}
.sidebar-slide-enter-from,
.sidebar-slide-leave-to {
transform: translateX(100%);
opacity: 0;
}
.sidebar-slide-enter-to,
.sidebar-slide-leave-from {
transform: translateX(0);
opacity: 1;
}
}

View File

@@ -84,6 +84,70 @@ export const useApi = () => {
return await request(url) return await request(url)
} }
const searchChatMessages = async (params = {}) => {
const query = new URLSearchParams()
if (params && params.account) query.set('account', params.account)
if (params && params.q) query.set('q', params.q)
if (params && params.username) query.set('username', params.username)
if (params && params.sender) query.set('sender', params.sender)
if (params && params.session_type) query.set('session_type', params.session_type)
if (params && params.limit != null) query.set('limit', String(params.limit))
if (params && params.offset != null) query.set('offset', String(params.offset))
if (params && params.start_time != null) query.set('start_time', String(params.start_time))
if (params && params.end_time != null) query.set('end_time', String(params.end_time))
if (params && params.render_types) query.set('render_types', params.render_types)
if (params && params.include_hidden != null) query.set('include_hidden', String(!!params.include_hidden))
if (params && params.include_official != null) query.set('include_official', String(!!params.include_official))
if (params && params.session_limit != null) query.set('session_limit', String(params.session_limit))
if (params && params.per_chat_scan != null) query.set('per_chat_scan', String(params.per_chat_scan))
if (params && params.scan_limit != null) query.set('scan_limit', String(params.scan_limit))
const url = '/chat/search' + (query.toString() ? `?${query.toString()}` : '')
return await request(url)
}
const listChatSearchSenders = async (params = {}) => {
const query = new URLSearchParams()
if (params && params.account) query.set('account', params.account)
if (params && params.username) query.set('username', params.username)
if (params && params.session_type) query.set('session_type', params.session_type)
if (params && params.limit != null) query.set('limit', String(params.limit))
if (params && params.q) query.set('q', params.q)
if (params && params.message_q) query.set('message_q', params.message_q)
if (params && params.start_time != null) query.set('start_time', String(params.start_time))
if (params && params.end_time != null) query.set('end_time', String(params.end_time))
if (params && params.render_types) query.set('render_types', params.render_types)
if (params && params.include_hidden != null) query.set('include_hidden', String(!!params.include_hidden))
if (params && params.include_official != null) query.set('include_official', String(!!params.include_official))
const url = '/chat/search-index/senders' + (query.toString() ? `?${query.toString()}` : '')
return await request(url)
}
const getChatSearchIndexStatus = async (params = {}) => {
const query = new URLSearchParams()
if (params && params.account) query.set('account', params.account)
const url = '/chat/search-index/status' + (query.toString() ? `?${query.toString()}` : '')
return await request(url)
}
const buildChatSearchIndex = async (params = {}) => {
const query = new URLSearchParams()
if (params && params.account) query.set('account', params.account)
if (params && params.rebuild != null) query.set('rebuild', String(!!params.rebuild))
const url = '/chat/search-index/build' + (query.toString() ? `?${query.toString()}` : '')
return await request(url, { method: 'POST' })
}
const getChatMessagesAround = async (params = {}) => {
const query = new URLSearchParams()
if (params && params.account) query.set('account', params.account)
if (params && params.username) query.set('username', params.username)
if (params && params.anchor_id) query.set('anchor_id', params.anchor_id)
if (params && params.before != null) query.set('before', String(params.before))
if (params && params.after != null) query.set('after', String(params.after))
const url = '/chat/messages/around' + (query.toString() ? `?${query.toString()}` : '')
return await request(url)
}
const openChatMediaFolder = async (params = {}) => { const openChatMediaFolder = async (params = {}) => {
const query = new URLSearchParams() const query = new URLSearchParams()
if (params && params.account) query.set('account', params.account) if (params && params.account) query.set('account', params.account)
@@ -108,23 +172,16 @@ export const useApi = () => {
}) })
} }
// 获取图片解密密钥
const getMediaKeys = async (params = {}) => {
const query = new URLSearchParams()
if (params && params.account) query.set('account', params.account)
if (params && params.force_extract) query.set('force_extract', 'true')
const url = '/media/keys' + (query.toString() ? `?${query.toString()}` : '')
return await request(url)
}
// 保存图片解密密钥 // 保存图片解密密钥
const saveMediaKeys = async (params = {}) => { const saveMediaKeys = async (params = {}) => {
const query = new URLSearchParams() return await request('/media/keys', {
if (params && params.account) query.set('account', params.account) method: 'POST',
if (params && params.xor_key) query.set('xor_key', params.xor_key) body: {
if (params && params.aes_key) query.set('aes_key', params.aes_key) account: params.account || null,
const url = '/media/keys' + (query.toString() ? `?${query.toString()}` : '') xor_key: params.xor_key || '',
return await request(url, { method: 'POST', body: { account: params.account, force_extract: false } }) aes_key: params.aes_key || null
}
})
} }
// 批量解密所有图片 // 批量解密所有图片
@@ -183,9 +240,13 @@ export const useApi = () => {
listChatAccounts, listChatAccounts,
listChatSessions, listChatSessions,
listChatMessages, listChatMessages,
searchChatMessages,
getChatSearchIndexStatus,
buildChatSearchIndex,
listChatSearchSenders,
getChatMessagesAround,
openChatMediaFolder, openChatMediaFolder,
downloadChatEmoji, downloadChatEmoji,
getMediaKeys,
saveMediaKeys, saveMediaKeys,
decryptAllMedia, decryptAllMedia,
createChatExport, createChatExport,

File diff suppressed because it is too large Load Diff

View File

@@ -55,7 +55,7 @@
<svg class="w-4 h-4 mr-1 text-[#10AEEF]" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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"/> <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> </svg>
使用 <a href="https://github.com/gzygood/DbkeyHook" target="_blank" class="text-[#07C160] hover:text-[#06AD56]">DbkeyHook</a> 等工具获取的64位十六进制字符串 使用 <a href="https://github.com/ycccccccy/wx_key" target="_blank" class="text-[#07C160] hover:text-[#06AD56]">wx_key</a> 等工具获取的64位十六进制字符串
</p> </p>
</div> </div>
@@ -113,7 +113,7 @@
</div> </div>
</div> </div>
<!-- 步骤2: 图片密钥获取 --> <!-- 步骤2: 填写图片密钥 -->
<div v-if="currentStep === 1" class="bg-white rounded-2xl border border-[#EDEDED]"> <div v-if="currentStep === 1" class="bg-white rounded-2xl border border-[#EDEDED]">
<div class="p-8"> <div class="p-8">
<div class="flex items-center mb-6"> <div class="flex items-center mb-6">
@@ -124,7 +124,7 @@
</div> </div>
<div> <div>
<h2 class="text-xl font-bold text-[#000000e6]">图片密钥</h2> <h2 class="text-xl font-bold text-[#000000e6]">图片密钥</h2>
<p class="text-sm text-[#7F7F7F]">获取图片解密所需的密钥</p> <p class="text-sm text-[#7F7F7F]">请使用 wx_key 获取后在此填写</p>
</div> </div>
</div> </div>
@@ -136,152 +136,64 @@
<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"/> <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> </svg>
<div class="text-sm text-blue-800"> <div class="text-sm text-blue-800">
<div class="font-medium mb-2">获取密钥小提示</div> 使用 <a href="https://github.com/ycccccccy/wx_key" target="_blank" class="underline">wx_key</a> 获取密钥AES 可选V4-V2 需要
<ul class="list-disc list-inside space-y-1">
<li><strong>AES 密钥</strong>仅在部分图片V4-V2解密时需要仅有 XOR 也可以先继续下一步失败原因会提示</li>
<li>获取 AES 需要微信正在运行部分环境需<strong>以管理员身份运行后端</strong>否则可能无法读取微信进程内存</li>
<li>若一直获取不到 AES完全退出微信 重新启动并登录 打开朋友圈图片并点开大图 2-3 回到本页点<strong>强制重新提取</strong></li>
</ul>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- 密钥信息显示 --> <!-- 填写密钥 -->
<div class="space-y-4 mb-6">
<div class="bg-gray-50 rounded-lg p-4">
<div class="flex justify-between items-center mb-2">
<span class="text-sm font-medium text-[#000000e6]">XOR 密钥</span>
<button
type="button"
class="font-mono text-sm px-3 py-1 bg-white rounded border border-[#EDEDED] transition-colors"
:class="mediaKeys.xor_key ? 'cursor-pointer hover:bg-gray-50' : 'cursor-not-allowed opacity-60'"
:title="mediaKeys.xor_key ? '点击复制' : ''"
@click="copyKey('XOR 密钥', mediaKeys.xor_key)"
>
{{ mediaKeys.xor_key || '未获取' }}
</button>
</div>
<div class="flex justify-between items-center">
<span class="text-sm font-medium text-[#000000e6]">AES 密钥</span>
<button
type="button"
class="font-mono text-sm px-3 py-1 bg-white rounded border border-[#EDEDED] transition-colors"
:class="mediaKeys.aes_key ? 'cursor-pointer hover:bg-gray-50' : 'cursor-not-allowed opacity-60'"
:title="mediaKeys.aes_key ? '点击复制' : ''"
@click="copyKey('AES 密钥', mediaKeys.aes_key)"
>
{{ mediaKeys.aes_key || '未获取' }}
</button>
</div>
</div>
<div v-if="mediaKeys.message" class="text-sm text-[#7F7F7F] flex items-start">
<svg class="w-4 h-4 mr-2 mt-0.5 flex-shrink-0 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>
{{ mediaKeys.message }}
</div>
<div v-if="copyMessage" class="text-sm text-[#07C160] flex items-start">
<svg class="w-4 h-4 mr-2 mt-0.5 flex-shrink-0" 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>
{{ copyMessage }}
</div>
</div>
<!-- 手动输入密钥自动获取失败时 -->
<div class="mb-6"> <div class="mb-6">
<details class="text-sm"> <div class="bg-gray-50 rounded-lg p-4">
<summary class="cursor-pointer text-[#7F7F7F] hover:text-[#000000e6]"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<span class="ml-1">手动输入密钥自动获取失败时</span> <div>
</summary> <label class="block text-sm font-medium text-[#000000e6] mb-2">XOR必填</label>
<div class="mt-3 bg-gray-50 rounded-lg p-4"> <input
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> v-model="manualKeys.xor_key"
<div> type="text"
<label class="block text-sm font-medium text-[#000000e6] mb-2">XOR 密钥 <span class="text-red-500">*</span></label> placeholder="例如0xA5"
<input class="w-full px-4 py-2 border border-[#EDEDED] rounded-lg focus:ring-2 focus:ring-[#10AEEF] focus:border-transparent font-mono"
v-model="manualKeys.xor_key" />
type="text" <p v-if="manualKeyErrors.xor_key" class="text-xs text-red-500 mt-1">{{ manualKeyErrors.xor_key }}</p>
placeholder="例如0xA5 或 A5"
class="w-full px-4 py-2 border border-[#EDEDED] rounded-lg focus:ring-2 focus:ring-[#10AEEF] focus:border-transparent font-mono"
/>
<p v-if="manualKeyErrors.xor_key" class="text-xs text-red-500 mt-1">{{ manualKeyErrors.xor_key }}</p>
</div>
<div>
<label class="block text-sm font-medium text-[#000000e6] mb-2">AES 密钥可选</label>
<input
v-model="manualKeys.aes_key"
type="text"
placeholder="16 个字符V4-V2 需要)"
class="w-full px-4 py-2 border border-[#EDEDED] rounded-lg focus:ring-2 focus:ring-[#10AEEF] focus:border-transparent font-mono"
/>
<p v-if="manualKeyErrors.aes_key" class="text-xs text-red-500 mt-1">{{ manualKeyErrors.aes_key }}</p>
</div>
</div> </div>
<div>
<div class="flex flex-wrap gap-3 mt-4"> <label class="block text-sm font-medium text-[#000000e6] mb-2">AES可选</label>
<button <input
type="button" v-model="manualKeys.aes_key"
@click="applyManualKeys({ save: false })" type="text"
class="inline-flex items-center px-4 py-2 bg-white text-[#10AEEF] border border-[#10AEEF] rounded-lg font-medium hover:bg-gray-50 transition-all duration-200" placeholder="16 个字符V4-V2 需要)"
> class="w-full px-4 py-2 border border-[#EDEDED] rounded-lg focus:ring-2 focus:ring-[#10AEEF] focus:border-transparent font-mono"
使用手动密钥 />
</button> <p v-if="manualKeyErrors.aes_key" class="text-xs text-red-500 mt-1">{{ manualKeyErrors.aes_key }}</p>
<button
type="button"
@click="applyManualKeys({ save: true })"
:disabled="manualSaving"
class="inline-flex items-center px-4 py-2 bg-[#10AEEF] text-white rounded-lg font-medium hover:bg-[#0D9BD9] transition-all duration-200 disabled:opacity-50"
>
{{ manualSaving ? '保存中...' : '保存并使用' }}
</button>
<button
type="button"
@click="clearManualKeys"
class="inline-flex items-center px-4 py-2 bg-white text-[#7F7F7F] border border-[#EDEDED] rounded-lg font-medium hover:bg-gray-50 transition-all duration-200"
>
清空
</button>
</div>
<div class="text-xs text-[#7F7F7F] mt-3">
<p>说明</p>
<ul class="list-disc list-inside space-y-1 mt-1">
<li>XOR 1 字节十六进制00-FF</li>
<li>AES 仅在部分图片V4-V2解密时需要输入任意 16 个字符即可会自动截取前 16 </li>
</ul>
</div> </div>
</div> </div>
</details>
<div class="flex flex-wrap gap-3 mt-4">
<button
type="button"
@click="applyManualKeys({ save: true })"
:disabled="manualSaving"
class="inline-flex items-center px-4 py-2 bg-[#10AEEF] text-white rounded-lg font-medium hover:bg-[#0D9BD9] transition-all duration-200 disabled:opacity-50"
>
{{ manualSaving ? '保存中...' : '保存' }}
</button>
<button
type="button"
@click="clearManualKeys"
class="inline-flex items-center px-4 py-2 bg-white text-[#7F7F7F] border border-[#EDEDED] rounded-lg font-medium hover:bg-gray-50 transition-all duration-200"
>
清空
</button>
</div>
<p v-if="mediaKeys.message" class="text-xs text-[#7F7F7F] mt-3">{{ mediaKeys.message }}</p>
</div>
</div> </div>
<!-- 操作按钮 --> <!-- 操作按钮 -->
<div class="flex gap-3 justify-center pt-4 border-t border-[#EDEDED]"> <div class="flex gap-3 justify-center pt-4 border-t border-[#EDEDED]">
<button <button
@click="fetchMediaKeys(false)" @click="goToMediaDecryptStep"
:disabled="mediaLoading"
class="inline-flex items-center px-6 py-3 bg-[#10AEEF] text-white rounded-lg font-medium hover:bg-[#0D9BD9] transition-all duration-200 disabled:opacity-50"
>
<svg v-if="mediaLoading" class="w-5 h-5 mr-2 animate-spin" 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 12h4z"></path>
</svg>
<svg v-else 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>
{{ mediaLoading ? '获取中...' : '获取密钥' }}
</button>
<button
@click="fetchMediaKeys(true)"
:disabled="mediaLoading"
class="inline-flex items-center px-6 py-3 bg-white text-[#10AEEF] border border-[#10AEEF] rounded-lg font-medium hover:bg-gray-50 transition-all duration-200 disabled:opacity-50"
>
强制重新提取
</button>
<button
@click="goToStep(2)"
class="inline-flex items-center px-6 py-3 bg-[#07C160] text-white rounded-lg font-medium hover:bg-[#06AD56] transition-all duration-200" class="inline-flex items-center px-6 py-3 bg-[#07C160] text-white rounded-lg font-medium hover:bg-[#06AD56] transition-all duration-200"
> >
下一步 下一步
@@ -403,7 +315,7 @@
<p class="mb-2">可能的失败原因</p> <p class="mb-2">可能的失败原因</p>
<ul class="list-disc list-inside space-y-1"> <ul class="list-disc list-inside space-y-1">
<li><strong>解密后非有效图片</strong>文件不是图片格式(如视频缩略图损坏)</li> <li><strong>解密后非有效图片</strong>文件不是图片格式(如视频缩略图损坏)</li>
<li><strong>V4-V2版本需要AES密钥</strong>需要微信运行且部分环境需以管理员身份运行后端才能提取可尝试打开朋友圈图片并点开大图 2-3 次后再提取</li> <li><strong>V4-V2版本需要AES密钥</strong>请使用 wx_key 获取 AES 密钥后再重试解密</li>
<li><strong>未知加密版本</strong>新版微信使用了不支持的加密方式</li> <li><strong>未知加密版本</strong>新版微信使用了不支持的加密方式</li>
<li><strong>文件为空</strong>原始文件损坏或为空文件</li> <li><strong>文件为空</strong>原始文件损坏或为空文件</li>
</ul> </ul>
@@ -482,16 +394,17 @@
import { ref, reactive, computed, onMounted } from 'vue' import { ref, reactive, computed, onMounted } from 'vue'
import { useApi } from '~/composables/useApi' import { useApi } from '~/composables/useApi'
const { decryptDatabase, getMediaKeys, decryptAllMedia, saveMediaKeys } = useApi() const { decryptDatabase, saveMediaKeys } = useApi()
const loading = ref(false) const loading = ref(false)
const error = ref('') const error = ref('')
const currentStep = ref(0) const currentStep = ref(0)
const mediaAccount = ref('')
// 步骤定义 // 步骤定义
const steps = [ const steps = [
{ title: '数据库解密' }, { title: '数据库解密' },
{ title: '图片密钥' }, { title: '填写图片密钥' },
{ title: '图片解密' } { title: '图片解密' }
] ]
@@ -513,11 +426,8 @@ const mediaKeys = reactive({
aes_key: '', aes_key: '',
message: '' message: ''
}) })
const mediaLoading = ref(false)
const copyMessage = ref('')
let copyMessageTimer = null
// 手动输入密钥(自动获取失败时使用 // 手动输入密钥(从 wx_key 获取
const manualKeys = reactive({ const manualKeys = reactive({
xor_key: '', xor_key: '',
aes_key: '' aes_key: ''
@@ -550,36 +460,39 @@ const applyManualKeys = async (options = { save: false }) => {
manualKeyErrors.aes_key = '' manualKeyErrors.aes_key = ''
error.value = '' error.value = ''
const xor = normalizeXorKey(manualKeys.xor_key)
if (!xor.ok) {
manualKeyErrors.xor_key = xor.message
return
}
const aes = normalizeAesKey(manualKeys.aes_key) const aes = normalizeAesKey(manualKeys.aes_key)
if (!aes.ok) { if (!aes.ok) {
manualKeyErrors.aes_key = aes.message manualKeyErrors.aes_key = aes.message
return return
} }
mediaKeys.xor_key = xor.value const hasXor = !!String(manualKeys.xor_key || '').trim()
mediaKeys.aes_key = aes.value if (options?.save || hasXor) {
mediaKeys.message = options?.save ? '已保存并使用手动密钥' : '已使用手动密钥(仅本次)' const xor = normalizeXorKey(manualKeys.xor_key)
if (!xor.ok) {
manualKeyErrors.xor_key = xor.message
return
}
mediaKeys.xor_key = xor.value
}
if (aes.value) {
mediaKeys.aes_key = aes.value
}
mediaKeys.message = options?.save ? '已保存' : '已应用'
if (!options?.save) return if (!options?.save) return
if (!aes.value) {
mediaKeys.message = '已使用手动密钥未保存AES 为空)'
return
}
try { try {
manualSaving.value = true manualSaving.value = true
await saveMediaKeys({ await saveMediaKeys({
xor_key: xor.value, account: mediaAccount.value || null,
aes_key: aes.value xor_key: mediaKeys.xor_key,
aes_key: aes.value || null
}) })
} catch (e) { } catch (e) {
mediaKeys.message = '已使用手动密钥(保存失败可继续解密)' mediaKeys.message = '保存失败可继续解密)'
} finally { } finally {
manualSaving.value = false manualSaving.value = false
} }
@@ -590,6 +503,9 @@ const clearManualKeys = () => {
manualKeys.aes_key = '' manualKeys.aes_key = ''
manualKeyErrors.xor_key = '' manualKeyErrors.xor_key = ''
manualKeyErrors.aes_key = '' manualKeyErrors.aes_key = ''
mediaKeys.xor_key = ''
mediaKeys.aes_key = ''
mediaKeys.message = ''
} }
// 图片解密相关 // 图片解密相关
@@ -665,10 +581,18 @@ const handleDecrypt = async () => {
if (process.client && typeof window !== 'undefined') { if (process.client && typeof window !== 'undefined') {
sessionStorage.setItem('decryptResult', JSON.stringify(result)) sessionStorage.setItem('decryptResult', JSON.stringify(result))
} }
// 进入图片密钥获取步骤 // 记录当前账号(用于图片解密/密钥保存)
try {
const accounts = Object.keys(result.account_results || {})
if (accounts.length > 0) mediaAccount.value = accounts[0]
} catch (e) {
// ignore
}
// 进入图片密钥填写步骤
clearManualKeys()
currentStep.value = 1 currentStep.value = 1
// 自动尝试获取图片密钥 mediaKeys.message = ''
fetchMediaKeys(false)
} else if (result.status === 'failed') { } else if (result.status === 'failed') {
if (result.failure_count > 0 && result.success_count === 0) { if (result.failure_count > 0 && result.success_count === 0) {
error.value = result.message || '所有文件解密失败' error.value = result.message || '所有文件解密失败'
@@ -685,75 +609,6 @@ const handleDecrypt = async () => {
} }
} }
// 获取图片密钥
const fetchMediaKeys = async (forceExtract = false) => {
mediaLoading.value = true
error.value = ''
try {
const result = await getMediaKeys({ force_extract: forceExtract })
if (result.status === 'success') {
mediaKeys.xor_key = result.xor_key || ''
mediaKeys.aes_key = result.aes_key || ''
mediaKeys.message = result.message || ''
} else {
error.value = result.message || '获取密钥失败'
}
} catch (err) {
error.value = err.message || '获取密钥过程中发生错误'
} finally {
mediaLoading.value = false
}
}
const _copyToClipboard = async (text) => {
if (!process.client || typeof window === 'undefined') return false
if (!text) return false
try {
if (navigator?.clipboard?.writeText) {
await navigator.clipboard.writeText(text)
return true
}
} catch (e) {
// Ignore and fallback below
}
try {
const textarea = document.createElement('textarea')
textarea.value = text
textarea.setAttribute('readonly', '')
textarea.style.position = 'fixed'
textarea.style.opacity = '0'
textarea.style.left = '-9999px'
textarea.style.top = '0'
document.body.appendChild(textarea)
textarea.select()
textarea.setSelectionRange(0, textarea.value.length)
const ok = document.execCommand('copy')
document.body.removeChild(textarea)
return ok
} catch (e) {
return false
}
}
const _setCopyMessage = (message) => {
copyMessage.value = message
if (copyMessageTimer) clearTimeout(copyMessageTimer)
copyMessageTimer = setTimeout(() => {
copyMessage.value = ''
copyMessageTimer = null
}, 2000)
}
const copyKey = async (label, value) => {
if (!value) return
const ok = await _copyToClipboard(value)
_setCopyMessage(ok ? `${label}已复制` : `${label}复制失败,请手动复制`)
}
// 批量解密所有图片使用SSE实时进度 // 批量解密所有图片使用SSE实时进度
const decryptAllImages = async () => { const decryptAllImages = async () => {
mediaDecrypting.value = true mediaDecrypting.value = true
@@ -773,6 +628,7 @@ const decryptAllImages = async () => {
try { try {
// 构建SSE URL // 构建SSE URL
const params = new URLSearchParams() const params = new URLSearchParams()
if (mediaAccount.value) params.set('account', mediaAccount.value)
if (mediaKeys.xor_key) params.set('xor_key', mediaKeys.xor_key) if (mediaKeys.xor_key) params.set('xor_key', mediaKeys.xor_key)
if (mediaKeys.aes_key) params.set('aes_key', mediaKeys.aes_key) if (mediaKeys.aes_key) params.set('aes_key', mediaKeys.aes_key)
const url = `http://localhost:8000/api/media/decrypt_all_stream?${params.toString()}` const url = `http://localhost:8000/api/media/decrypt_all_stream?${params.toString()}`
@@ -830,10 +686,15 @@ const decryptAllImages = async () => {
} }
} }
// 跳转到指定步骤 // 从密钥步骤进入图片解密步骤
const goToStep = (step) => { const goToMediaDecryptStep = async () => {
currentStep.value = step
error.value = '' error.value = ''
// 用户填写了任一项时,尝试校验并应用(未填写则允许直接进入,后端会使用已保存密钥或报错提示)
if (manualKeys.xor_key || manualKeys.aes_key) {
await applyManualKeys({ save: false })
if (manualKeyErrors.xor_key || manualKeyErrors.aes_key) return
}
currentStep.value = 2
} }
// 跳过图片解密,直接查看聊天记录 // 跳过图片解密,直接查看聊天记录
@@ -852,6 +713,9 @@ onMounted(() => {
if (account.data_dir) { if (account.data_dir) {
formData.db_storage_path = account.data_dir + '\\db_storage' formData.db_storage_path = account.data_dir + '\\db_storage'
} }
if (account.account_name) {
mediaAccount.value = account.account_name
}
// 清除sessionStorage // 清除sessionStorage
sessionStorage.removeItem('selectedAccount') sessionStorage.removeItem('selectedAccount')
} catch (e) { } catch (e) {

View File

@@ -208,11 +208,12 @@ import { ref, onMounted, computed } from 'vue'
import { useApi } from '~/composables/useApi' import { useApi } from '~/composables/useApi'
import { useAppStore } from '~/stores/app' import { useAppStore } from '~/stores/app'
const { detectWechat, detectCurrentAccount } = useApi() const { detectWechat } = useApi()
const appStore = useAppStore() const appStore = useAppStore()
const loading = ref(false) const loading = ref(false)
const detectionResult = ref(null) const detectionResult = ref(null)
const customPath = ref('') const customPath = ref('')
const STORAGE_KEY = 'wechat_data_root_path'
// 计算属性:将当前登录账号排在第一位 // 计算属性:将当前登录账号排在第一位
const sortedAccounts = computed(() => { const sortedAccounts = computed(() => {
@@ -244,24 +245,51 @@ const startDetection = async () => {
} }
// 检测微信安装信息 // 检测微信安装信息
const result = await detectWechat(params) let result = await detectWechat(params)
// 如果用户提供/缓存的路径不可用,自动回退到“自动检测”(避免因错误缓存导致一直检测不到)
const hasCustomPath = !!(params.data_root_path && String(params.data_root_path).trim())
const accounts0 = Array.isArray(result?.data?.accounts) ? result.data.accounts : []
if (hasCustomPath && (result?.status !== 'success' || accounts0.length === 0)) {
try {
const fallback = await detectWechat({})
const accounts1 = Array.isArray(fallback?.data?.accounts) ? fallback.data.accounts : []
if (fallback?.status === 'success' && accounts1.length > 0) {
result = fallback
if (process.client) {
try {
localStorage.removeItem(STORAGE_KEY)
} catch {}
}
customPath.value = ''
}
} catch {}
}
detectionResult.value = result detectionResult.value = result
// 如果检测成功,同时检测当前登录账号
if (result.status === 'success') { if (result.status === 'success') {
try { const current = result?.data?.current_account || null
const currentAccountResult = await detectCurrentAccount(params) if (current) {
if (currentAccountResult.status === 'success') { appStore.setCurrentAccount(current)
// 保存当前账号信息到状态管理 }
appStore.setCurrentAccount(currentAccountResult.data)
if (process.client) {
// 同时更新检测结果中的当前账号信息 try {
if (detectionResult.value.data) { let toSave = String(customPath.value || '').trim()
detectionResult.value.data.current_account = currentAccountResult.data if (!toSave) {
const accounts = Array.isArray(result?.data?.accounts) ? result.data.accounts : []
for (const acc of accounts) {
const dataDir = String(acc?.data_dir || '').trim()
if (!dataDir) continue
toSave = dataDir.replace(/[\\/][^\\/]+$/, '')
if (toSave) break
}
} }
} if (toSave) {
} catch (accountErr) { localStorage.setItem(STORAGE_KEY, toSave)
console.error('检测当前登录账号失败:', accountErr) }
} catch {}
} }
} }
} catch (err) { } catch (err) {
@@ -322,6 +350,12 @@ const formatTime = (timeString) => {
// 页面加载时自动检测 // 页面加载时自动检测
onMounted(() => { onMounted(() => {
if (process.client) {
try {
const saved = String(localStorage.getItem(STORAGE_KEY) || '').trim()
if (saved) customPath.value = saved
} catch {}
}
startDetection() startDetection()
// 调试:检查各元素高度 // 调试:检查各元素高度
@@ -368,4 +402,4 @@ onMounted(() => {
linear-gradient(90deg, 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; background-size: 50px 50px;
} }
</style> </style>