mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
Merge branch 'dev'
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
ManifestDPIAware true
|
ManifestDPIAware true
|
||||||
|
|
||||||
!include "WordFunc.nsh"
|
!include "WordFunc.nsh"
|
||||||
|
!include "nsDialogs.nsh"
|
||||||
|
|
||||||
!macro customInit
|
!macro customInit
|
||||||
; 设置 DPI 感知
|
; 设置 DPI 感知
|
||||||
@@ -16,3 +17,49 @@ ManifestDPIAware true
|
|||||||
StrCpy $INSTDIR "$INSTDIR\WeFlow"
|
StrCpy $INSTDIR "$INSTDIR\WeFlow"
|
||||||
${EndIf}
|
${EndIf}
|
||||||
!macroend
|
!macroend
|
||||||
|
|
||||||
|
; 安装完成后检测并安装 VC++ Redistributable
|
||||||
|
!macro customInstall
|
||||||
|
; 检查 VC++ 2015-2022 x64 是否已安装
|
||||||
|
ReadRegStr $0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64" "Installed"
|
||||||
|
${If} $0 != "1"
|
||||||
|
; 未安装,显示提示并下载
|
||||||
|
MessageBox MB_YESNO|MB_ICONQUESTION "检测到系统缺少 Visual C++ 运行库,这可能导致程序无法正常运行。$\n$\n是否立即下载并安装?(约 24MB)" IDYES downloadVC IDNO skipVC
|
||||||
|
|
||||||
|
downloadVC:
|
||||||
|
DetailPrint "正在下载 Visual C++ Redistributable..."
|
||||||
|
SetOutPath "$TEMP"
|
||||||
|
|
||||||
|
; 从微软官方下载 VC++ Redistributable x64
|
||||||
|
inetc::get /TIMEOUT=30000 /CAPTION "下载 Visual C++ 运行库" /BANNER "正在下载,请稍候..." \
|
||||||
|
"https://aka.ms/vs/17/release/vc_redist.x64.exe" "$TEMP\vc_redist.x64.exe" /END
|
||||||
|
Pop $0
|
||||||
|
|
||||||
|
${If} $0 == "OK"
|
||||||
|
DetailPrint "下载完成,正在安装..."
|
||||||
|
; 使用 ShellExecute 以管理员权限运行
|
||||||
|
ExecShell "runas" '"$TEMP\vc_redist.x64.exe"' "/install /quiet /norestart" SW_HIDE
|
||||||
|
; 等待安装完成
|
||||||
|
Sleep 5000
|
||||||
|
; 检查是否安装成功
|
||||||
|
ReadRegStr $1 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64" "Installed"
|
||||||
|
${If} $1 == "1"
|
||||||
|
DetailPrint "Visual C++ Redistributable 安装成功"
|
||||||
|
MessageBox MB_OK|MB_ICONINFORMATION "Visual C++ 运行库安装成功!"
|
||||||
|
${Else}
|
||||||
|
MessageBox MB_OK|MB_ICONEXCLAMATION "Visual C++ 运行库安装失败,您可能需要手动安装。"
|
||||||
|
${EndIf}
|
||||||
|
Delete "$TEMP\vc_redist.x64.exe"
|
||||||
|
${Else}
|
||||||
|
MessageBox MB_OK|MB_ICONEXCLAMATION "下载失败:$0$\n$\n您可以稍后手动下载安装 Visual C++ Redistributable。"
|
||||||
|
${EndIf}
|
||||||
|
Goto doneVC
|
||||||
|
|
||||||
|
skipVC:
|
||||||
|
DetailPrint "用户跳过 Visual C++ Redistributable 安装"
|
||||||
|
|
||||||
|
doneVC:
|
||||||
|
${Else}
|
||||||
|
DetailPrint "Visual C++ Redistributable 已安装"
|
||||||
|
${EndIf}
|
||||||
|
!macroend
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "weflow",
|
"name": "weflow",
|
||||||
"version": "1.1.0",
|
"version": "1.1.2",
|
||||||
"description": "WeFlow",
|
"description": "WeFlow",
|
||||||
"main": "dist-electron/main.js",
|
"main": "dist-electron/main.js",
|
||||||
"author": "cc",
|
"author": "cc",
|
||||||
|
|||||||
46
src/components/ImagePreview.scss
Normal file
46
src/components/ImagePreview.scss
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
.image-preview-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 90vh;
|
||||||
|
object-fit: contain;
|
||||||
|
transition: transform 0.15s ease-out;
|
||||||
|
|
||||||
|
&.dragging {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-close {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 40px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-color: rgba(255, 255, 255, 0.5);
|
||||||
|
transform: translateX(-50%) scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
101
src/components/ImagePreview.tsx
Normal file
101
src/components/ImagePreview.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import React, { useState, useRef, useCallback, useEffect } from 'react'
|
||||||
|
import { X } from 'lucide-react'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
import './ImagePreview.scss'
|
||||||
|
|
||||||
|
interface ImagePreviewProps {
|
||||||
|
src: string
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ImagePreview: React.FC<ImagePreviewProps> = ({ src, onClose }) => {
|
||||||
|
const [scale, setScale] = useState(1)
|
||||||
|
const [position, setPosition] = useState({ x: 0, y: 0 })
|
||||||
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
|
const dragStart = useRef({ x: 0, y: 0 })
|
||||||
|
const positionStart = useRef({ x: 0, y: 0 })
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
// 滚轮缩放
|
||||||
|
const handleWheel = useCallback((e: React.WheelEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const delta = e.deltaY > 0 ? 0.9 : 1.1
|
||||||
|
setScale(prev => Math.min(Math.max(prev * delta, 0.5), 5))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 开始拖动
|
||||||
|
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||||
|
if (scale <= 1) return
|
||||||
|
e.preventDefault()
|
||||||
|
setIsDragging(true)
|
||||||
|
dragStart.current = { x: e.clientX, y: e.clientY }
|
||||||
|
positionStart.current = { ...position }
|
||||||
|
}, [scale, position])
|
||||||
|
|
||||||
|
// 拖动中
|
||||||
|
const handleMouseMove = useCallback((e: React.MouseEvent) => {
|
||||||
|
if (!isDragging) return
|
||||||
|
const dx = e.clientX - dragStart.current.x
|
||||||
|
const dy = e.clientY - dragStart.current.y
|
||||||
|
setPosition({
|
||||||
|
x: positionStart.current.x + dx,
|
||||||
|
y: positionStart.current.y + dy
|
||||||
|
})
|
||||||
|
}, [isDragging])
|
||||||
|
|
||||||
|
// 结束拖动
|
||||||
|
const handleMouseUp = useCallback(() => {
|
||||||
|
setIsDragging(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 双击重置
|
||||||
|
const handleDoubleClick = useCallback(() => {
|
||||||
|
setScale(1)
|
||||||
|
setPosition({ x: 0, y: 0 })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 点击背景关闭
|
||||||
|
const handleOverlayClick = useCallback((e: React.MouseEvent) => {
|
||||||
|
if (e.target === containerRef.current) {
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}, [onClose])
|
||||||
|
|
||||||
|
// ESC 关闭
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') onClose()
|
||||||
|
}
|
||||||
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||||
|
}, [onClose])
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="image-preview-overlay"
|
||||||
|
onClick={handleOverlayClick}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
|
onMouseLeave={handleMouseUp}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt="图片预览"
|
||||||
|
className={`preview-image ${isDragging ? 'dragging' : ''}`}
|
||||||
|
style={{
|
||||||
|
transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`,
|
||||||
|
cursor: scale > 1 ? (isDragging ? 'grabbing' : 'grab') : 'default'
|
||||||
|
}}
|
||||||
|
onWheel={handleWheel}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onDoubleClick={handleDoubleClick}
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
<button className="image-preview-close" onClick={onClose}>
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1291,36 +1291,6 @@
|
|||||||
color: var(--text-quaternary);
|
color: var(--text-quaternary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-preview-overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.75);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1000;
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 88vw;
|
|
||||||
max-height: 88vh;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.35);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-preview-close {
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: none;
|
|
||||||
background: rgba(0, 0, 0, 0.6);
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 语音消息
|
// 语音消息
|
||||||
.voice-message {
|
.voice-message {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createPortal } from 'react-dom'
|
|||||||
import { useChatStore } from '../stores/chatStore'
|
import { useChatStore } from '../stores/chatStore'
|
||||||
import type { ChatSession, Message } from '../types/models'
|
import type { ChatSession, Message } from '../types/models'
|
||||||
import { getEmojiPath } from 'wechat-emojis'
|
import { getEmojiPath } from 'wechat-emojis'
|
||||||
|
import { ImagePreview } from '../components/ImagePreview'
|
||||||
import './ChatPage.scss'
|
import './ChatPage.scss'
|
||||||
|
|
||||||
interface ChatPageProps {
|
interface ChatPageProps {
|
||||||
@@ -1682,14 +1683,8 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat }:
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{showImagePreview && createPortal(
|
{showImagePreview && (
|
||||||
<div className="image-preview-overlay" onClick={() => setShowImagePreview(false)}>
|
<ImagePreview src={imageLocalPath} onClose={() => setShowImagePreview(false)} />
|
||||||
<img src={imageLocalPath} alt="图片预览" onClick={(e) => e.stopPropagation()} />
|
|
||||||
<button className="image-preview-close" onClick={() => setShowImagePreview(false)}>
|
|
||||||
<X size={16} />
|
|
||||||
</button>
|
|
||||||
</div>,
|
|
||||||
document.body
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -767,3 +767,168 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// wxid 输入框下拉
|
||||||
|
.wxid-input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex: 1;
|
||||||
|
padding-right: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wxid-dropdown-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wxid-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin-top: 4px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 100;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wxid-option {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: var(--primary-light);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wxid-value {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wxid-time {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多账号选择对话框
|
||||||
|
.wxid-dialog-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wxid-dialog {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: 16px;
|
||||||
|
width: 400px;
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wxid-dialog-header {
|
||||||
|
padding: 20px 24px;
|
||||||
|
border-bottom: 1px solid var(--border-primary);
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wxid-dialog-list {
|
||||||
|
padding: 8px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wxid-dialog-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: var(--primary-light);
|
||||||
|
|
||||||
|
.wxid-id {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wxid-id {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wxid-date {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wxid-dialog-footer {
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-top: 1px solid var(--border-primary);
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { useAppStore } from '../stores/appStore'
|
import { useAppStore } from '../stores/appStore'
|
||||||
import { useThemeStore, themes } from '../stores/themeStore'
|
import { useThemeStore, themes } from '../stores/themeStore'
|
||||||
import { dialog } from '../services/ipc'
|
import { dialog } from '../services/ipc'
|
||||||
@@ -6,7 +6,7 @@ import * as configService from '../services/config'
|
|||||||
import {
|
import {
|
||||||
Eye, EyeOff, FolderSearch, FolderOpen, Search, Copy,
|
Eye, EyeOff, FolderSearch, FolderOpen, Search, Copy,
|
||||||
RotateCcw, Trash2, Save, Plug, Check, Sun, Moon,
|
RotateCcw, Trash2, Save, Plug, Check, Sun, Moon,
|
||||||
Palette, Database, Download, HardDrive, Info, RefreshCw
|
Palette, Database, Download, HardDrive, Info, RefreshCw, ChevronDown
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import './SettingsPage.scss'
|
import './SettingsPage.scss'
|
||||||
|
|
||||||
@@ -19,6 +19,11 @@ const tabs: { id: SettingsTab; label: string; icon: React.ElementType }[] = [
|
|||||||
{ id: 'about', label: '关于', icon: Info }
|
{ id: 'about', label: '关于', icon: Info }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
interface WxidOption {
|
||||||
|
wxid: string
|
||||||
|
modifiedTime: number
|
||||||
|
}
|
||||||
|
|
||||||
function SettingsPage() {
|
function SettingsPage() {
|
||||||
const { setDbConnected, setLoading, reset } = useAppStore()
|
const { setDbConnected, setLoading, reset } = useAppStore()
|
||||||
const { currentTheme, themeMode, setTheme, setThemeMode } = useThemeStore()
|
const { currentTheme, themeMode, setTheme, setThemeMode } = useThemeStore()
|
||||||
@@ -29,6 +34,9 @@ function SettingsPage() {
|
|||||||
const [imageAesKey, setImageAesKey] = useState('')
|
const [imageAesKey, setImageAesKey] = useState('')
|
||||||
const [dbPath, setDbPath] = useState('')
|
const [dbPath, setDbPath] = useState('')
|
||||||
const [wxid, setWxid] = useState('')
|
const [wxid, setWxid] = useState('')
|
||||||
|
const [wxidOptions, setWxidOptions] = useState<WxidOption[]>([])
|
||||||
|
const [showWxidSelect, setShowWxidSelect] = useState(false)
|
||||||
|
const wxidDropdownRef = useRef<HTMLDivElement>(null)
|
||||||
const [cachePath, setCachePath] = useState('')
|
const [cachePath, setCachePath] = useState('')
|
||||||
const [logEnabled, setLogEnabled] = useState(false)
|
const [logEnabled, setLogEnabled] = useState(false)
|
||||||
|
|
||||||
@@ -53,6 +61,17 @@ function SettingsPage() {
|
|||||||
loadAppVersion()
|
loadAppVersion()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// 点击外部关闭下拉框
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
if (showWxidSelect && wxidDropdownRef.current && !wxidDropdownRef.current.contains(e.target as Node)) {
|
||||||
|
setShowWxidSelect(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', handleClickOutside)
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||||
|
}, [showWxidSelect])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const removeDb = window.electronAPI.key.onDbKeyStatus((payload) => {
|
const removeDb = window.electronAPI.key.onDbKeyStatus((payload) => {
|
||||||
setDbKeyStatus(payload.message)
|
setDbKeyStatus(payload.message)
|
||||||
@@ -156,12 +175,14 @@ function SettingsPage() {
|
|||||||
showMessage(`自动检测成功:${result.path}`, true)
|
showMessage(`自动检测成功:${result.path}`, true)
|
||||||
|
|
||||||
const wxids = await window.electronAPI.dbPath.scanWxids(result.path)
|
const wxids = await window.electronAPI.dbPath.scanWxids(result.path)
|
||||||
|
setWxidOptions(wxids)
|
||||||
if (wxids.length === 1) {
|
if (wxids.length === 1) {
|
||||||
setWxid(wxids[0].wxid)
|
setWxid(wxids[0].wxid)
|
||||||
await configService.setMyWxid(wxids[0].wxid)
|
await configService.setMyWxid(wxids[0].wxid)
|
||||||
showMessage(`已检测到账号:${wxids[0].wxid}`, true)
|
showMessage(`已检测到账号:${wxids[0].wxid}`, true)
|
||||||
} else if (wxids.length > 1) {
|
} else if (wxids.length > 1) {
|
||||||
showMessage(`检测到 ${wxids.length} 个账号,请手动选择`, true)
|
// 多账号时弹出选择对话框
|
||||||
|
setShowWxidSelect(true)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showMessage(result.error || '未能自动检测到数据库目录', false)
|
showMessage(result.error || '未能自动检测到数据库目录', false)
|
||||||
@@ -192,12 +213,14 @@ function SettingsPage() {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const wxids = await window.electronAPI.dbPath.scanWxids(dbPath)
|
const wxids = await window.electronAPI.dbPath.scanWxids(dbPath)
|
||||||
|
setWxidOptions(wxids)
|
||||||
if (wxids.length === 1) {
|
if (wxids.length === 1) {
|
||||||
setWxid(wxids[0].wxid)
|
setWxid(wxids[0].wxid)
|
||||||
await configService.setMyWxid(wxids[0].wxid)
|
await configService.setMyWxid(wxids[0].wxid)
|
||||||
if (!silent) showMessage(`已检测到账号:${wxids[0].wxid}`, true)
|
if (!silent) showMessage(`已检测到账号:${wxids[0].wxid}`, true)
|
||||||
} else if (wxids.length > 1) {
|
} else if (wxids.length > 1) {
|
||||||
if (!silent) showMessage(`检测到 ${wxids.length} 个账号,请手动选择`, true)
|
// 多账号时弹出选择对话框
|
||||||
|
setShowWxidSelect(true)
|
||||||
} else {
|
} else {
|
||||||
if (!silent) showMessage('未检测到账号目录,请检查路径', false)
|
if (!silent) showMessage('未检测到账号目录,请检查路径', false)
|
||||||
}
|
}
|
||||||
@@ -206,6 +229,13 @@ function SettingsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSelectWxid = async (selectedWxid: string) => {
|
||||||
|
setWxid(selectedWxid)
|
||||||
|
await configService.setMyWxid(selectedWxid)
|
||||||
|
setShowWxidSelect(false)
|
||||||
|
showMessage(`已选择账号:${selectedWxid}`, true)
|
||||||
|
}
|
||||||
|
|
||||||
const handleSelectCachePath = async () => {
|
const handleSelectCachePath = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await dialog.openFile({ title: '选择缓存目录', properties: ['openDirectory'] })
|
const result = await dialog.openFile({ title: '选择缓存目录', properties: ['openDirectory'] })
|
||||||
@@ -466,7 +496,38 @@ function SettingsPage() {
|
|||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>账号 wxid</label>
|
<label>账号 wxid</label>
|
||||||
<span className="form-hint">微信账号标识</span>
|
<span className="form-hint">微信账号标识</span>
|
||||||
<input type="text" placeholder="例如: wxid_xxxxxx" value={wxid} onChange={(e) => setWxid(e.target.value)} />
|
<div className="wxid-input-wrapper" ref={wxidDropdownRef}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="例如: wxid_xxxxxx"
|
||||||
|
value={wxid}
|
||||||
|
onChange={(e) => setWxid(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`wxid-dropdown-btn ${showWxidSelect ? 'open' : ''}`}
|
||||||
|
onClick={() => wxidOptions.length > 0 ? setShowWxidSelect(!showWxidSelect) : handleScanWxid()}
|
||||||
|
title={wxidOptions.length > 0 ? "选择已检测到的账号" : "扫描账号"}
|
||||||
|
>
|
||||||
|
<ChevronDown size={16} />
|
||||||
|
</button>
|
||||||
|
{showWxidSelect && wxidOptions.length > 0 && (
|
||||||
|
<div className="wxid-dropdown">
|
||||||
|
{wxidOptions.map((opt) => (
|
||||||
|
<div
|
||||||
|
key={opt.wxid}
|
||||||
|
className={`wxid-option ${opt.wxid === wxid ? 'active' : ''}`}
|
||||||
|
onClick={() => handleSelectWxid(opt.wxid)}
|
||||||
|
>
|
||||||
|
<span className="wxid-value">{opt.wxid}</span>
|
||||||
|
<span className="wxid-time">
|
||||||
|
{new Date(opt.modifiedTime).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<button className="btn btn-secondary btn-sm" onClick={() => handleScanWxid()}><Search size={14} /> 扫描 wxid</button>
|
<button className="btn btn-secondary btn-sm" onClick={() => handleScanWxid()}><Search size={14} /> 扫描 wxid</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -604,6 +665,33 @@ function SettingsPage() {
|
|||||||
<div className="settings-page">
|
<div className="settings-page">
|
||||||
{message && <div className={`message-toast ${message.success ? 'success' : 'error'}`}>{message.text}</div>}
|
{message && <div className={`message-toast ${message.success ? 'success' : 'error'}`}>{message.text}</div>}
|
||||||
|
|
||||||
|
{/* 多账号选择对话框 */}
|
||||||
|
{showWxidSelect && wxidOptions.length > 1 && (
|
||||||
|
<div className="wxid-dialog-overlay" onClick={() => setShowWxidSelect(false)}>
|
||||||
|
<div className="wxid-dialog" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="wxid-dialog-header">
|
||||||
|
<h3>检测到多个微信账号</h3>
|
||||||
|
<p>请选择要使用的账号</p>
|
||||||
|
</div>
|
||||||
|
<div className="wxid-dialog-list">
|
||||||
|
{wxidOptions.map((opt) => (
|
||||||
|
<div
|
||||||
|
key={opt.wxid}
|
||||||
|
className={`wxid-dialog-item ${opt.wxid === wxid ? 'active' : ''}`}
|
||||||
|
onClick={() => handleSelectWxid(opt.wxid)}
|
||||||
|
>
|
||||||
|
<span className="wxid-id">{opt.wxid}</span>
|
||||||
|
<span className="wxid-date">最后修改: {new Date(opt.modifiedTime).toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="wxid-dialog-footer">
|
||||||
|
<button className="btn btn-secondary" onClick={() => setShowWxidSelect(false)}>取消</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="settings-header">
|
<div className="settings-header">
|
||||||
<h1>设置</h1>
|
<h1>设置</h1>
|
||||||
<div className="settings-actions">
|
<div className="settings-actions">
|
||||||
|
|||||||
Reference in New Issue
Block a user