mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
新增询问窗口
This commit is contained in:
@@ -99,6 +99,8 @@ let isAppQuitting = false
|
||||
let tray: Tray | null = null
|
||||
let isClosePromptVisible = false
|
||||
|
||||
type WindowCloseBehavior = 'ask' | 'tray' | 'quit'
|
||||
|
||||
// 更新下载状态管理(Issue #294 修复)
|
||||
let isDownloadInProgress = false
|
||||
let downloadProgressHandler: ((progress: any) => void) | null = null
|
||||
@@ -254,6 +256,19 @@ const setupCustomTitleBarWindow = (win: BrowserWindow): void => {
|
||||
win.webContents.on('did-finish-load', emitMaximizeState)
|
||||
}
|
||||
|
||||
const getWindowCloseBehavior = (): WindowCloseBehavior => {
|
||||
const behavior = configService?.get('windowCloseBehavior')
|
||||
return behavior === 'tray' || behavior === 'quit' ? behavior : 'ask'
|
||||
}
|
||||
|
||||
const requestMainWindowCloseConfirmation = (win: BrowserWindow): void => {
|
||||
if (isClosePromptVisible) return
|
||||
isClosePromptVisible = true
|
||||
win.webContents.send('window:confirmCloseRequested', {
|
||||
canMinimizeToTray: Boolean(tray)
|
||||
})
|
||||
}
|
||||
|
||||
function createWindow(options: { autoShow?: boolean } = {}) {
|
||||
// 获取图标路径 - 打包后在 resources 目录
|
||||
const { autoShow = true } = options
|
||||
@@ -357,12 +372,20 @@ function createWindow(options: { autoShow?: boolean } = {}) {
|
||||
win.on('close', (e) => {
|
||||
if (isAppQuitting || win !== mainWindow) return
|
||||
e.preventDefault()
|
||||
if (isClosePromptVisible) return
|
||||
const closeBehavior = getWindowCloseBehavior()
|
||||
|
||||
isClosePromptVisible = true
|
||||
win.webContents.send('window:confirmCloseRequested', {
|
||||
canMinimizeToTray: Boolean(tray)
|
||||
})
|
||||
if (closeBehavior === 'quit') {
|
||||
isAppQuitting = true
|
||||
app.quit()
|
||||
return
|
||||
}
|
||||
|
||||
if (closeBehavior === 'tray' && tray) {
|
||||
win.hide()
|
||||
return
|
||||
}
|
||||
|
||||
requestMainWindowCloseConfirmation(win)
|
||||
})
|
||||
|
||||
win.on('closed', () => {
|
||||
|
||||
@@ -50,6 +50,7 @@ interface ConfigSchema {
|
||||
notificationPosition: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center'
|
||||
notificationFilterMode: 'all' | 'whitelist' | 'blacklist'
|
||||
notificationFilterList: string[]
|
||||
windowCloseBehavior: 'ask' | 'tray' | 'quit'
|
||||
wordCloudExcludeWords: string[]
|
||||
}
|
||||
|
||||
@@ -116,6 +117,7 @@ export class ConfigService {
|
||||
notificationPosition: 'top-right',
|
||||
notificationFilterMode: 'all',
|
||||
notificationFilterList: [],
|
||||
windowCloseBehavior: 'ask',
|
||||
wordCloudExcludeWords: []
|
||||
}
|
||||
})
|
||||
|
||||
16
src/App.tsx
16
src/App.tsx
@@ -327,8 +327,19 @@ function App() {
|
||||
setUpdateInfo(null)
|
||||
}
|
||||
|
||||
const handleWindowCloseAction = async (action: 'tray' | 'quit' | 'cancel') => {
|
||||
const handleWindowCloseAction = async (
|
||||
action: 'tray' | 'quit' | 'cancel',
|
||||
rememberChoice = false
|
||||
) => {
|
||||
setShowCloseDialog(false)
|
||||
if (rememberChoice && action !== 'cancel') {
|
||||
try {
|
||||
await configService.setWindowCloseBehavior(action)
|
||||
} catch (error) {
|
||||
console.error('保存关闭偏好失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await window.electronAPI.window.respondCloseConfirm(action)
|
||||
} catch (error) {
|
||||
@@ -617,8 +628,7 @@ function App() {
|
||||
<WindowCloseDialog
|
||||
open={showCloseDialog}
|
||||
canMinimizeToTray={canMinimizeToTray}
|
||||
onTray={() => handleWindowCloseAction('tray')}
|
||||
onQuit={() => handleWindowCloseAction('quit')}
|
||||
onSelect={(action, rememberChoice) => handleWindowCloseAction(action, rememberChoice)}
|
||||
onCancel={() => handleWindowCloseAction('cancel')}
|
||||
/>
|
||||
|
||||
|
||||
@@ -140,6 +140,72 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.window-close-dialog-remember {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 4px 24px 0;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 72%, transparent);
|
||||
border-radius: 16px;
|
||||
background: color-mix(in srgb, var(--bg-secondary) 76%, transparent);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.window-close-dialog-checkbox {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex: 0 0 18px;
|
||||
border: 1.5px solid color-mix(in srgb, var(--border-color) 88%, transparent);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-primary);
|
||||
position: relative;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
background 0.18s ease,
|
||||
box-shadow 0.18s ease;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 1px;
|
||||
width: 5px;
|
||||
height: 10px;
|
||||
border: solid white;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg) scale(0.7);
|
||||
opacity: 0;
|
||||
transition:
|
||||
opacity 0.18s ease,
|
||||
transform 0.18s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.window-close-dialog-remember input:checked + .window-close-dialog-checkbox {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 16%, transparent);
|
||||
}
|
||||
|
||||
.window-close-dialog-remember input:checked + .window-close-dialog-checkbox::after {
|
||||
opacity: 1;
|
||||
transform: rotate(45deg) scale(1);
|
||||
}
|
||||
|
||||
.window-close-dialog-remember-text {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.window-close-dialog-cancel {
|
||||
min-width: 112px;
|
||||
padding: 12px 18px;
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import { Minimize2, Power, X } from 'lucide-react'
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import './WindowCloseDialog.scss'
|
||||
|
||||
interface WindowCloseDialogProps {
|
||||
open: boolean
|
||||
canMinimizeToTray: boolean
|
||||
onTray: () => void
|
||||
onQuit: () => void
|
||||
onSelect: (action: 'tray' | 'quit', rememberChoice: boolean) => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export default function WindowCloseDialog({
|
||||
open,
|
||||
canMinimizeToTray,
|
||||
onTray,
|
||||
onQuit,
|
||||
onSelect,
|
||||
onCancel
|
||||
}: WindowCloseDialogProps) {
|
||||
const [rememberChoice, setRememberChoice] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
setRememberChoice(false)
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
@@ -63,7 +64,11 @@ export default function WindowCloseDialog({
|
||||
|
||||
<div className="window-close-dialog-body">
|
||||
{canMinimizeToTray && (
|
||||
<button type="button" className="window-close-dialog-option" onClick={onTray}>
|
||||
<button
|
||||
type="button"
|
||||
className="window-close-dialog-option"
|
||||
onClick={() => onSelect('tray', rememberChoice)}
|
||||
>
|
||||
<span className="window-close-dialog-option-icon">
|
||||
<Minimize2 size={18} />
|
||||
</span>
|
||||
@@ -77,7 +82,7 @@ export default function WindowCloseDialog({
|
||||
<button
|
||||
type="button"
|
||||
className="window-close-dialog-option is-danger"
|
||||
onClick={onQuit}
|
||||
onClick={() => onSelect('quit', rememberChoice)}
|
||||
>
|
||||
<span className="window-close-dialog-option-icon">
|
||||
<Power size={18} />
|
||||
@@ -89,6 +94,16 @@ export default function WindowCloseDialog({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label className="window-close-dialog-remember">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={rememberChoice}
|
||||
onChange={(event) => setRememberChoice(event.target.checked)}
|
||||
/>
|
||||
<span className="window-close-dialog-checkbox" aria-hidden="true" />
|
||||
<span className="window-close-dialog-remember-text">下次不再提示,直接按本次选择处理</span>
|
||||
</label>
|
||||
|
||||
<div className="window-close-dialog-actions">
|
||||
<button type="button" className="window-close-dialog-cancel" onClick={onCancel}>
|
||||
取消
|
||||
|
||||
@@ -107,9 +107,11 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
const [notificationPosition, setNotificationPosition] = useState<'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center'>('top-right')
|
||||
const [notificationFilterMode, setNotificationFilterMode] = useState<'all' | 'whitelist' | 'blacklist'>('all')
|
||||
const [notificationFilterList, setNotificationFilterList] = useState<string[]>([])
|
||||
const [windowCloseBehavior, setWindowCloseBehavior] = useState<configService.WindowCloseBehavior>('ask')
|
||||
const [filterSearchKeyword, setFilterSearchKeyword] = useState('')
|
||||
const [filterModeDropdownOpen, setFilterModeDropdownOpen] = useState(false)
|
||||
const [positionDropdownOpen, setPositionDropdownOpen] = useState(false)
|
||||
const [closeBehaviorDropdownOpen, setCloseBehaviorDropdownOpen] = useState(false)
|
||||
|
||||
const [wordCloudExcludeWords, setWordCloudExcludeWords] = useState<string[]>([])
|
||||
const [excludeWordsInput, setExcludeWordsInput] = useState('')
|
||||
@@ -253,15 +255,16 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
if (!target.closest('.custom-select')) {
|
||||
setFilterModeDropdownOpen(false)
|
||||
setPositionDropdownOpen(false)
|
||||
setCloseBehaviorDropdownOpen(false)
|
||||
}
|
||||
}
|
||||
if (filterModeDropdownOpen || positionDropdownOpen) {
|
||||
if (filterModeDropdownOpen || positionDropdownOpen || closeBehaviorDropdownOpen) {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
}
|
||||
}, [filterModeDropdownOpen, positionDropdownOpen])
|
||||
}, [closeBehaviorDropdownOpen, filterModeDropdownOpen, positionDropdownOpen])
|
||||
|
||||
|
||||
const loadConfig = async () => {
|
||||
@@ -283,6 +286,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
const savedNotificationPosition = await configService.getNotificationPosition()
|
||||
const savedNotificationFilterMode = await configService.getNotificationFilterMode()
|
||||
const savedNotificationFilterList = await configService.getNotificationFilterList()
|
||||
const savedWindowCloseBehavior = await configService.getWindowCloseBehavior()
|
||||
|
||||
const savedAuthEnabled = await window.electronAPI.auth.verifyEnabled()
|
||||
const savedAuthUseHello = await configService.getAuthUseHello()
|
||||
@@ -318,6 +322,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
setNotificationPosition(savedNotificationPosition)
|
||||
setNotificationFilterMode(savedNotificationFilterMode)
|
||||
setNotificationFilterList(savedNotificationFilterList)
|
||||
setWindowCloseBehavior(savedWindowCloseBehavior)
|
||||
|
||||
const savedExcludeWords = await configService.getWordCloudExcludeWords()
|
||||
setWordCloudExcludeWords(savedExcludeWords)
|
||||
@@ -1024,6 +1029,61 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="divider" />
|
||||
|
||||
<div className="form-group">
|
||||
<label>关闭主窗口时</label>
|
||||
<span className="form-hint">设置点击关闭按钮后的默认行为;选择“每次询问”时会弹出关闭确认。</span>
|
||||
<div className="custom-select">
|
||||
<div
|
||||
className={`custom-select-trigger ${closeBehaviorDropdownOpen ? 'open' : ''}`}
|
||||
onClick={() => setCloseBehaviorDropdownOpen(!closeBehaviorDropdownOpen)}
|
||||
>
|
||||
<span className="custom-select-value">
|
||||
{windowCloseBehavior === 'tray'
|
||||
? '最小化到系统托盘'
|
||||
: windowCloseBehavior === 'quit'
|
||||
? '完全关闭'
|
||||
: '每次询问'}
|
||||
</span>
|
||||
<ChevronDown size={14} className={`custom-select-arrow ${closeBehaviorDropdownOpen ? 'rotate' : ''}`} />
|
||||
</div>
|
||||
<div className={`custom-select-dropdown ${closeBehaviorDropdownOpen ? 'open' : ''}`}>
|
||||
{[
|
||||
{
|
||||
value: 'ask' as const,
|
||||
label: '每次询问',
|
||||
successMessage: '已恢复关闭确认弹窗'
|
||||
},
|
||||
{
|
||||
value: 'tray' as const,
|
||||
label: '最小化到系统托盘',
|
||||
successMessage: '关闭按钮已改为最小化到托盘'
|
||||
},
|
||||
{
|
||||
value: 'quit' as const,
|
||||
label: '完全关闭',
|
||||
successMessage: '关闭按钮已改为完全关闭'
|
||||
}
|
||||
].map(option => (
|
||||
<div
|
||||
key={option.value}
|
||||
className={`custom-select-option ${windowCloseBehavior === option.value ? 'selected' : ''}`}
|
||||
onClick={async () => {
|
||||
setWindowCloseBehavior(option.value)
|
||||
setCloseBehaviorDropdownOpen(false)
|
||||
await configService.setWindowCloseBehavior(option.value)
|
||||
showMessage(option.successMessage, true)
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
{windowCloseBehavior === option.value && <Check size={14} />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ export const CONFIG_KEYS = {
|
||||
NOTIFICATION_POSITION: 'notificationPosition',
|
||||
NOTIFICATION_FILTER_MODE: 'notificationFilterMode',
|
||||
NOTIFICATION_FILTER_LIST: 'notificationFilterList',
|
||||
WINDOW_CLOSE_BEHAVIOR: 'windowCloseBehavior',
|
||||
|
||||
// 词云
|
||||
WORD_CLOUD_EXCLUDE_WORDS: 'wordCloudExcludeWords',
|
||||
@@ -85,6 +86,8 @@ export interface ExportDefaultMediaConfig {
|
||||
emojis: boolean
|
||||
}
|
||||
|
||||
export type WindowCloseBehavior = 'ask' | 'tray' | 'quit'
|
||||
|
||||
const DEFAULT_EXPORT_MEDIA_CONFIG: ExportDefaultMediaConfig = {
|
||||
images: true,
|
||||
videos: true,
|
||||
@@ -1188,6 +1191,16 @@ export async function setNotificationFilterList(list: string[]): Promise<void> {
|
||||
await config.set(CONFIG_KEYS.NOTIFICATION_FILTER_LIST, list)
|
||||
}
|
||||
|
||||
export async function getWindowCloseBehavior(): Promise<WindowCloseBehavior> {
|
||||
const value = await config.get(CONFIG_KEYS.WINDOW_CLOSE_BEHAVIOR)
|
||||
if (value === 'tray' || value === 'quit') return value
|
||||
return 'ask'
|
||||
}
|
||||
|
||||
export async function setWindowCloseBehavior(behavior: WindowCloseBehavior): Promise<void> {
|
||||
await config.set(CONFIG_KEYS.WINDOW_CLOSE_BEHAVIOR, behavior)
|
||||
}
|
||||
|
||||
// 获取词云排除词列表
|
||||
export async function getWordCloudExcludeWords(): Promise<string[]> {
|
||||
const value = await config.get(CONFIG_KEYS.WORD_CLOUD_EXCLUDE_WORDS)
|
||||
|
||||
Reference in New Issue
Block a user