mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
feat(ui): 新增支持缩放、拖拽及键盘控制的图片预览组件并优化样式,更新安装程序支持 VC++ 运行库自动安装,多账号支持列表选择。
This commit is contained in:
@@ -1291,36 +1291,6 @@
|
||||
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 {
|
||||
display: flex;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createPortal } from 'react-dom'
|
||||
import { useChatStore } from '../stores/chatStore'
|
||||
import type { ChatSession, Message } from '../types/models'
|
||||
import { getEmojiPath } from 'wechat-emojis'
|
||||
import { ImagePreview } from '../components/ImagePreview'
|
||||
import './ChatPage.scss'
|
||||
|
||||
interface ChatPageProps {
|
||||
@@ -1682,14 +1683,8 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat }:
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{showImagePreview && createPortal(
|
||||
<div className="image-preview-overlay" onClick={() => 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
|
||||
{showImagePreview && (
|
||||
<ImagePreview src={imageLocalPath} onClose={() => setShowImagePreview(false)} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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 { useThemeStore, themes } from '../stores/themeStore'
|
||||
import { dialog } from '../services/ipc'
|
||||
@@ -6,7 +6,7 @@ import * as configService from '../services/config'
|
||||
import {
|
||||
Eye, EyeOff, FolderSearch, FolderOpen, Search, Copy,
|
||||
RotateCcw, Trash2, Save, Plug, Check, Sun, Moon,
|
||||
Palette, Database, Download, HardDrive, Info, RefreshCw
|
||||
Palette, Database, Download, HardDrive, Info, RefreshCw, ChevronDown
|
||||
} from 'lucide-react'
|
||||
import './SettingsPage.scss'
|
||||
|
||||
@@ -19,6 +19,11 @@ const tabs: { id: SettingsTab; label: string; icon: React.ElementType }[] = [
|
||||
{ id: 'about', label: '关于', icon: Info }
|
||||
]
|
||||
|
||||
interface WxidOption {
|
||||
wxid: string
|
||||
modifiedTime: number
|
||||
}
|
||||
|
||||
function SettingsPage() {
|
||||
const { setDbConnected, setLoading, reset } = useAppStore()
|
||||
const { currentTheme, themeMode, setTheme, setThemeMode } = useThemeStore()
|
||||
@@ -29,6 +34,9 @@ function SettingsPage() {
|
||||
const [imageAesKey, setImageAesKey] = useState('')
|
||||
const [dbPath, setDbPath] = 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 [logEnabled, setLogEnabled] = useState(false)
|
||||
|
||||
@@ -53,6 +61,17 @@ function SettingsPage() {
|
||||
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(() => {
|
||||
const removeDb = window.electronAPI.key.onDbKeyStatus((payload) => {
|
||||
setDbKeyStatus(payload.message)
|
||||
@@ -156,12 +175,14 @@ function SettingsPage() {
|
||||
showMessage(`自动检测成功:${result.path}`, true)
|
||||
|
||||
const wxids = await window.electronAPI.dbPath.scanWxids(result.path)
|
||||
setWxidOptions(wxids)
|
||||
if (wxids.length === 1) {
|
||||
setWxid(wxids[0].wxid)
|
||||
await configService.setMyWxid(wxids[0].wxid)
|
||||
showMessage(`已检测到账号:${wxids[0].wxid}`, true)
|
||||
} else if (wxids.length > 1) {
|
||||
showMessage(`检测到 ${wxids.length} 个账号,请手动选择`, true)
|
||||
// 多账号时弹出选择对话框
|
||||
setShowWxidSelect(true)
|
||||
}
|
||||
} else {
|
||||
showMessage(result.error || '未能自动检测到数据库目录', false)
|
||||
@@ -192,12 +213,14 @@ function SettingsPage() {
|
||||
}
|
||||
try {
|
||||
const wxids = await window.electronAPI.dbPath.scanWxids(dbPath)
|
||||
setWxidOptions(wxids)
|
||||
if (wxids.length === 1) {
|
||||
setWxid(wxids[0].wxid)
|
||||
await configService.setMyWxid(wxids[0].wxid)
|
||||
if (!silent) showMessage(`已检测到账号:${wxids[0].wxid}`, true)
|
||||
} else if (wxids.length > 1) {
|
||||
if (!silent) showMessage(`检测到 ${wxids.length} 个账号,请手动选择`, true)
|
||||
// 多账号时弹出选择对话框
|
||||
setShowWxidSelect(true)
|
||||
} else {
|
||||
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 () => {
|
||||
try {
|
||||
const result = await dialog.openFile({ title: '选择缓存目录', properties: ['openDirectory'] })
|
||||
@@ -466,7 +496,38 @@ function SettingsPage() {
|
||||
<div className="form-group">
|
||||
<label>账号 wxid</label>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -604,6 +665,33 @@ function SettingsPage() {
|
||||
<div className="settings-page">
|
||||
{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">
|
||||
<h1>设置</h1>
|
||||
<div className="settings-actions">
|
||||
|
||||
Reference in New Issue
Block a user