diff --git a/src/main/data/PreferenceService.ts b/src/main/data/PreferenceService.ts
index fd14c1bdca..0c2f52294d 100644
--- a/src/main/data/PreferenceService.ts
+++ b/src/main/data/PreferenceService.ts
@@ -1,3 +1,4 @@
+import { dbService } from '@data/db/DbService'
import { loggerService } from '@logger'
import { DefaultPreferences } from '@shared/data/preferences'
import type { PreferenceDefaultScopeType, PreferenceKeyType } from '@shared/data/types'
@@ -5,7 +6,6 @@ import { IpcChannel } from '@shared/IpcChannel'
import { and, eq } from 'drizzle-orm'
import { BrowserWindow } from 'electron'
-import dbService from './db/DbService'
import { preferenceTable } from './db/schemas/preference'
const logger = loggerService.withContext('PreferenceService')
@@ -296,4 +296,3 @@ export class PreferenceService {
// Export singleton instance
export const preferenceService = PreferenceService.getInstance()
-export default preferenceService
diff --git a/src/main/data/db/DbService.ts b/src/main/data/db/DbService.ts
index 00d87a6ba0..6f136a58a6 100644
--- a/src/main/data/db/DbService.ts
+++ b/src/main/data/db/DbService.ts
@@ -67,6 +67,4 @@ class DbService {
}
// Export a singleton instance
-const dbService = DbService.getInstance()
-
-export default dbService
+export const dbService = DbService.getInstance()
diff --git a/src/main/data/migrate/dataRefactor/DataRefactorMigrateService.ts b/src/main/data/migrate/dataRefactor/DataRefactorMigrateService.ts
index db922bd613..eb748b7667 100644
--- a/src/main/data/migrate/dataRefactor/DataRefactorMigrateService.ts
+++ b/src/main/data/migrate/dataRefactor/DataRefactorMigrateService.ts
@@ -1,4 +1,4 @@
-import dbService from '@data/db/DbService'
+import { dbService } from '@data/db/DbService'
import { appStateTable } from '@data/db/schemas/appState'
import { loggerService } from '@logger'
import { isDev } from '@main/constant'
@@ -48,6 +48,7 @@ interface MigrationResult {
export class DataRefactorMigrateService {
private static instance: DataRefactorMigrateService | null = null
private migrateWindow: BrowserWindow | null = null
+ private testWindows: BrowserWindow[] = []
private backupManager: BackupManager
private db = dbService.getDb()
private currentProgress: MigrationProgress = {
@@ -302,6 +303,14 @@ export class DataRefactorMigrateService {
return DataRefactorMigrateService.instance
}
+ /**
+ * Convenient static method to open test window
+ */
+ public static openTestWindow(): BrowserWindow {
+ const instance = DataRefactorMigrateService.getInstance()
+ return instance.createTestWindow()
+ }
+
/**
* Check if migration is needed
*/
@@ -862,6 +871,112 @@ export class DataRefactorMigrateService {
// Don't throw - still allow restart
}
}
+
+ /**
+ * Create and show test window for testing PreferenceService and usePreference functionality
+ */
+ public createTestWindow(): BrowserWindow {
+ const windowNumber = this.testWindows.length + 1
+
+ const testWindow = new BrowserWindow({
+ width: 1000,
+ height: 700,
+ minWidth: 800,
+ minHeight: 600,
+ resizable: true,
+ maximizable: true,
+ minimizable: true,
+ show: false,
+ frame: true,
+ autoHideMenuBar: true,
+ title: `Data Refactor Test Window #${windowNumber} - PreferenceService Testing`,
+ webPreferences: {
+ preload: join(__dirname, '../preload/index.js'),
+ sandbox: false,
+ webSecurity: false,
+ contextIsolation: true
+ }
+ })
+
+ // Add to test windows array
+ this.testWindows.push(testWindow)
+
+ // Load the test window
+ if (isDev && process.env['ELECTRON_RENDERER_URL']) {
+ testWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/dataRefactorTest.html')
+ // Open DevTools in development mode for easier testing
+ testWindow.webContents.openDevTools()
+ } else {
+ testWindow.loadFile(join(__dirname, '../renderer/dataRefactorTest.html'))
+ }
+
+ testWindow.once('ready-to-show', () => {
+ testWindow?.show()
+ testWindow?.focus()
+ })
+
+ testWindow.on('closed', () => {
+ // Remove from test windows array when closed
+ const index = this.testWindows.indexOf(testWindow)
+ if (index > -1) {
+ this.testWindows.splice(index, 1)
+ }
+ })
+
+ logger.info(`Test window #${windowNumber} created for PreferenceService testing`)
+ return testWindow
+ }
+
+ /**
+ * Get test window instance (first one)
+ */
+ public getTestWindow(): BrowserWindow | null {
+ return this.testWindows.length > 0 ? this.testWindows[0] : null
+ }
+
+ /**
+ * Get all test windows
+ */
+ public getTestWindows(): BrowserWindow[] {
+ return this.testWindows.filter((window) => !window.isDestroyed())
+ }
+
+ /**
+ * Close all test windows
+ */
+ public closeTestWindows(): void {
+ this.testWindows.forEach((window) => {
+ if (!window.isDestroyed()) {
+ window.close()
+ }
+ })
+ this.testWindows = []
+ logger.info('All test windows closed')
+ }
+
+ /**
+ * Close a specific test window
+ */
+ public closeTestWindow(window?: BrowserWindow): void {
+ if (window) {
+ if (!window.isDestroyed()) {
+ window.close()
+ }
+ } else {
+ // Close first window if no specific window provided
+ const firstWindow = this.getTestWindow()
+ if (firstWindow && !firstWindow.isDestroyed()) {
+ firstWindow.close()
+ }
+ }
+ }
+
+ /**
+ * Check if any test windows are open
+ */
+ public isTestWindowOpen(): boolean {
+ return this.testWindows.some((window) => !window.isDestroyed())
+ }
}
// Export singleton instance
diff --git a/src/main/data/migrate/dataRefactor/migrators/PreferencesMigrator.ts b/src/main/data/migrate/dataRefactor/migrators/PreferencesMigrator.ts
index 3fff882706..a6c9581fd3 100644
--- a/src/main/data/migrate/dataRefactor/migrators/PreferencesMigrator.ts
+++ b/src/main/data/migrate/dataRefactor/migrators/PreferencesMigrator.ts
@@ -1,4 +1,4 @@
-import dbService from '@data/db/DbService'
+import { dbService } from '@data/db/DbService'
import { preferenceTable } from '@data/db/schemas/preference'
import { loggerService } from '@logger'
import { DefaultPreferences } from '@shared/data/preferences'
diff --git a/src/main/index.ts b/src/main/index.ts
index 52bc54e78e..3900a61971 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -7,7 +7,8 @@ import '@main/config'
import { loggerService } from '@logger'
import { electronApp, optimizer } from '@electron-toolkit/utils'
-import dbService from '@data/db/DbService'
+import { dbService } from '@data/db/DbService'
+import { preferenceService } from '@data/PreferenceService'
import { replaceDevtoolsFont } from '@main/utils/windowUtil'
import { app, dialog } from 'electron'
import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer'
@@ -154,6 +155,22 @@ if (!app.requestSingleInstanceLock()) {
return
}
+ //************FOR TESTING ONLY START****************/
+
+ await preferenceService.initialize()
+
+ // Create two test windows for cross-window preference sync testing
+ logger.info('Creating test windows for PreferenceService cross-window sync testing')
+ const testWindow1 = dataRefactorMigrateService.createTestWindow()
+ const testWindow2 = dataRefactorMigrateService.createTestWindow()
+
+ // Position windows to avoid overlap
+ testWindow1.once('ready-to-show', () => {
+ const [x, y] = testWindow1.getPosition()
+ testWindow2.setPosition(x + 50, y + 50)
+ })
+ /************FOR TESTING ONLY END****************/
+
// Set app user model id for windows
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
diff --git a/src/main/ipc.ts b/src/main/ipc.ts
index 20450f82e8..7c05b60ecd 100644
--- a/src/main/ipc.ts
+++ b/src/main/ipc.ts
@@ -2,6 +2,7 @@ import fs from 'node:fs'
import { arch } from 'node:os'
import path from 'node:path'
+import { preferenceService } from '@data/PreferenceService'
import { loggerService } from '@logger'
import { isLinux, isMac, isPortable, isWin } from '@main/constant'
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
@@ -14,7 +15,6 @@ import { FileMetadata, Provider, Shortcut, ThemeMode } from '@types'
import { BrowserWindow, dialog, ipcMain, ProxyConfig, session, shell, systemPreferences, webContents } from 'electron'
import { Notification } from 'src/renderer/src/types/notification'
-import preferenceService from './data/PreferenceService'
import appService from './services/AppService'
import AppUpdater from './services/AppUpdater'
import BackupManager from './services/BackupManager'
diff --git a/src/renderer/dataRefactorTest.html b/src/renderer/dataRefactorTest.html
new file mode 100644
index 0000000000..20309b3526
--- /dev/null
+++ b/src/renderer/dataRefactorTest.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+ Data Refactor Test Window - PreferenceService Testing
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/renderer/src/data/hooks/usePreference.ts b/src/renderer/src/data/hooks/usePreference.ts
index 0e2ecb851b..1324099053 100644
--- a/src/renderer/src/data/hooks/usePreference.ts
+++ b/src/renderer/src/data/hooks/usePreference.ts
@@ -1,6 +1,6 @@
import { loggerService } from '@logger'
import type { PreferenceDefaultScopeType, PreferenceKeyType } from '@shared/data/types'
-import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react'
+import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'
import { preferenceService } from '../PreferenceService'
@@ -18,8 +18,8 @@ export function usePreference(
): [PreferenceDefaultScopeType[K] | undefined, (value: PreferenceDefaultScopeType[K]) => Promise] {
// Subscribe to changes for this specific preference
const value = useSyncExternalStore(
- preferenceService.subscribeToKey(key),
- preferenceService.getSnapshot(key),
+ useCallback((callback) => preferenceService.subscribeToKey(key)(callback), [key]),
+ useCallback(() => preferenceService.getCachedValue(key), [key]),
() => undefined // SSR snapshot (not used in Electron context)
)
@@ -61,12 +61,16 @@ export function usePreferences>(
{ [P in keyof T]: PreferenceDefaultScopeType[T[P]] | undefined },
(updates: Partial<{ [P in keyof T]: PreferenceDefaultScopeType[T[P]] }>) => Promise
] {
- // Track changes to any of the specified keys
+ // Create stable key dependencies
const keyList = useMemo(() => Object.values(keys), [keys])
- const keyListString = keyList.join(',')
+ const keysStringified = useMemo(() => JSON.stringify(keys), [keys])
+
+ // Cache the last snapshot to avoid infinite loops
+ const lastSnapshotRef = useRef>({})
+
const allValues = useSyncExternalStore(
useCallback(
- (callback) => {
+ (callback: () => void) => {
// Subscribe to all keys and aggregate the unsubscribe functions
const unsubscribeFunctions = keyList.map((key) => preferenceService.subscribeToKey(key)(callback))
@@ -74,17 +78,30 @@ export function usePreferences>(
unsubscribeFunctions.forEach((unsubscribe) => unsubscribe())
}
},
- [keyList]
+ [keysStringified]
),
useCallback(() => {
- // Return current snapshot of all values
- const snapshot: Record = {}
+ // Check if any values have actually changed
+ let hasChanged = Object.keys(lastSnapshotRef.current).length === 0 // First time
+ const newSnapshot: Record = {}
+
for (const [localKey, prefKey] of Object.entries(keys)) {
- snapshot[localKey] = preferenceService.getCachedValue(prefKey)
+ const currentValue = preferenceService.getCachedValue(prefKey)
+ newSnapshot[localKey] = currentValue
+
+ if (!hasChanged && lastSnapshotRef.current[localKey] !== currentValue) {
+ hasChanged = true
+ }
}
- return snapshot
- }, [keys]),
+
+ // Only create new object if data actually changed
+ if (hasChanged) {
+ lastSnapshotRef.current = newSnapshot
+ }
+
+ return lastSnapshotRef.current
+ }, [keysStringified]),
() => ({}) // SSR snapshot
)
@@ -98,7 +115,7 @@ export function usePreferences>(
logger.error('Failed to load initial preferences:', error as Error)
})
}
- }, [keyList, keyListString])
+ }, [keysStringified])
// Memoized batch update function
const updateValues = useCallback(
@@ -119,7 +136,7 @@ export function usePreferences>(
throw error
}
},
- [keys]
+ [keysStringified]
)
// Type-cast the values to the expected shape
@@ -135,12 +152,12 @@ export function usePreferences>(
* @param keys - Array of preference keys to preload
*/
export function usePreferencePreload(keys: PreferenceKeyType[]): void {
- const keysString = keys.join(',')
+ const keysString = useMemo(() => keys.join(','), [keys])
useEffect(() => {
preferenceService.preload(keys).catch((error) => {
logger.error('Failed to preload preferences:', error as Error)
})
- }, [keys, keysString])
+ }, [keysString])
}
/**
diff --git a/src/renderer/src/windows/dataRefactorTest/README.md b/src/renderer/src/windows/dataRefactorTest/README.md
new file mode 100644
index 0000000000..798c72f0c1
--- /dev/null
+++ b/src/renderer/src/windows/dataRefactorTest/README.md
@@ -0,0 +1,118 @@
+# PreferenceService 测试窗口
+
+专用于测试 PreferenceService 和 usePreference hooks 功能的独立测试窗口系统。
+
+## 🎯 当前实现
+
+✅ **已完成的功能**:
+- 专用的测试窗口 (DataRefactorTestWindow)
+- **双窗口启动**:应用启动时会同时打开主窗口和两个测试窗口
+- **跨窗口同步测试**:两个测试窗口可以相互验证偏好设置的实时同步
+- 完整的测试界面,包含4个专业测试组件
+- 自动窗口定位,避免重叠
+- 窗口编号标识,便于区分
+
+## 测试组件
+
+### 1. PreferenceService 基础测试
+- 直接测试服务层API:get, set, getCachedValue, isCached, preload, getMultiple
+- 支持各种数据类型:字符串、数字、布尔值、JSON对象
+- 实时显示操作结果
+
+### 2. usePreference Hook 测试
+- 测试单个偏好设置的React hooks
+- 支持的测试键:
+ - `app.theme.mode` - 主题模式
+ - `app.language` - 语言设置
+ - `app.spell_check.enabled` - 拼写检查
+ - `app.zoom_factor` - 缩放因子
+- 实时值更新和类型转换
+
+### 3. usePreferences 批量操作测试
+- 测试多个偏好设置的批量管理
+- 4种预设场景:基础设置、UI设置、用户设置、自定义组合
+- 批量更新功能,支持JSON格式输入
+- 快速切换操作
+
+### 4. Hook 高级功能测试
+- 预加载机制测试
+- 订阅机制验证
+- 缓存管理测试
+- 性能测试
+- 多个hook实例同步测试
+
+## 启动方式
+
+**自动启动**:应用正常启动时会自动创建两个测试窗口,窗口会自动错位显示避免重叠
+
+**手动启动**:
+```javascript
+// 在开发者控制台中执行 - 创建单个测试窗口
+const { dataRefactorMigrateService } = require('./out/main/data/migrate/dataRefactor/DataRefactorMigrateService')
+dataRefactorMigrateService.createTestWindow()
+
+// 创建多个测试窗口
+dataRefactorMigrateService.createTestWindow() // 第二个窗口
+dataRefactorMigrateService.createTestWindow() // 第三个窗口...
+
+// 关闭所有测试窗口
+dataRefactorMigrateService.closeTestWindows()
+```
+
+## 文件结构
+
+```
+src/renderer/src/windows/dataRefactorTest/
+├── entryPoint.tsx # 窗口入口
+├── TestApp.tsx # 主应用组件
+└── components/
+ ├── PreferenceServiceTests.tsx # 服务层测试
+ ├── PreferenceBasicTests.tsx # 基础Hook测试
+ ├── PreferenceHookTests.tsx # 高级Hook测试
+ └── PreferenceMultipleTests.tsx # 批量操作测试
+```
+
+## 跨窗口同步测试
+
+🔄 **测试场景**:
+1. **实时同步验证**:在窗口#1中修改某个偏好设置,立即观察窗口#2是否同步更新
+2. **并发修改测试**:在两个窗口中快速连续修改同一设置,验证数据一致性
+3. **批量操作同步**:在一个窗口中批量更新多个设置,观察另一个窗口的同步表现
+4. **Hook实例同步**:验证多个usePreference hook实例是否正确同步
+
+📋 **测试步骤**:
+1. 同时打开两个测试窗口(自动启动)
+2. 选择相同的偏好设置键进行测试
+3. 在窗口#1中修改值,观察窗口#2的反应
+4. 检查"Hook 高级功能测试"中的订阅触发次数是否增加
+5. 验证缓存状态和实时数据的一致性
+
+## 注意事项
+
+⚠️ **所有测试操作都会影响真实的数据库存储!**
+
+- 测试使用真实的偏好设置系统
+- 修改的值会同步到主应用和所有测试窗口
+- 可以在主应用、测试窗口#1、测试窗口#2之间看到实时同步效果
+
+## 开发模式特性
+
+- 自动打开DevTools便于调试
+- 支持热重载
+- 完整的TypeScript类型检查
+- React DevTools支持
+
+## 💡 快速开始
+
+1. **启动应用** - 自动打开2个测试窗口
+2. **选择测试** - 在"usePreference Hook 测试"中选择要测试的偏好设置键
+3. **跨窗口验证** - 在窗口#1中修改值,观察窗口#2是否同步
+4. **高级测试** - 使用"Hook 高级功能测试"验证订阅和缓存机制
+5. **批量测试** - 使用"usePreferences 批量操作测试"进行多项设置测试
+
+## 🔧 技术实现
+
+- **窗口管理**: DataRefactorMigrateService 单例管理多个测试窗口
+- **数据同步**: 基于真实的 PreferenceService 和 IPC 通信
+- **UI框架**: Ant Design + styled-components + React 18
+- **类型安全**: 完整的 TypeScript 类型检查和偏好设置键约束
\ No newline at end of file
diff --git a/src/renderer/src/windows/dataRefactorTest/TestApp.tsx b/src/renderer/src/windows/dataRefactorTest/TestApp.tsx
new file mode 100644
index 0000000000..8e41e86bf8
--- /dev/null
+++ b/src/renderer/src/windows/dataRefactorTest/TestApp.tsx
@@ -0,0 +1,153 @@
+import { AppLogo } from '@renderer/config/env'
+import { loggerService } from '@renderer/services/LoggerService'
+import { Button, Card, Col, Divider, Layout, Row, Space, Typography } from 'antd'
+import { Database, FlaskConical, Settings, TestTube } from 'lucide-react'
+import React from 'react'
+import styled from 'styled-components'
+
+import PreferenceBasicTests from './components/PreferenceBasicTests'
+import PreferenceHookTests from './components/PreferenceHookTests'
+import PreferenceMultipleTests from './components/PreferenceMultipleTests'
+import PreferenceServiceTests from './components/PreferenceServiceTests'
+
+const { Header, Content } = Layout
+const { Title, Text } = Typography
+
+const logger = loggerService.withContext('TestApp')
+
+const TestApp: React.FC = () => {
+ // Get window title to determine window number
+ const windowTitle = document.title
+ const windowMatch = windowTitle.match(/#(\d+)/)
+ const windowNumber = windowMatch ? windowMatch[1] : '1'
+
+ return (
+
+
+
+
+
+
+ Test Window #{windowNumber}
+
+
+
+
+ Cross-Window Sync Testing
+
+
+
+
+
+
+
+ {/* Introduction Card */}
+
+
+
+
+
+
+ PreferenceService 功能测试 (窗口 #{windowNumber})
+
+
+
+ 此测试窗口用于验证 PreferenceService 和 usePreference hooks
+ 的各项功能,包括单个偏好设置的读写、多个偏好设置的批量操作、跨窗口数据同步等。
+
+ 测试使用的都是真实的偏好设置系统,所有操作都会影响实际的数据库存储。
+
+ 📋 跨窗口测试指南:在一个窗口中修改偏好设置,观察其他窗口是否实时同步更新。
+
+
+
+
+
+ {/* PreferenceService Basic Tests */}
+
+
+
+ PreferenceService 基础测试
+
+ }
+ size="small">
+
+
+
+
+ {/* Basic Hook Tests */}
+
+
+
+ usePreference Hook 测试
+
+ }
+ size="small">
+
+
+
+
+ {/* Hook Tests */}
+
+
+
+ Hook 高级功能测试
+
+ }
+ size="small">
+
+
+
+
+ {/* Multiple Preferences Tests */}
+
+
+
+ usePreferences 批量操作测试
+
+ }
+ size="small">
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+const HeaderContent = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ height: 100%;
+`
+
+const Container = styled.div`
+ max-width: 1200px;
+ margin: 0 auto;
+`
+
+export default TestApp
diff --git a/src/renderer/src/windows/dataRefactorTest/components/PreferenceBasicTests.tsx b/src/renderer/src/windows/dataRefactorTest/components/PreferenceBasicTests.tsx
new file mode 100644
index 0000000000..b302e787e4
--- /dev/null
+++ b/src/renderer/src/windows/dataRefactorTest/components/PreferenceBasicTests.tsx
@@ -0,0 +1,213 @@
+import { usePreference } from '@renderer/data/hooks/usePreference'
+import type { PreferenceKeyType } from '@shared/data/types'
+import { Button, Input, message, Select, Space, Switch, Typography } from 'antd'
+import React, { useState } from 'react'
+import styled from 'styled-components'
+
+const { Text } = Typography
+const { Option } = Select
+
+/**
+ * Basic usePreference hook testing component
+ * Tests single preference management with React hooks
+ */
+const PreferenceBasicTests: React.FC = () => {
+ const [selectedKey, setSelectedKey] = useState('app.theme.mode')
+
+ // Use the hook with the selected key
+ const [value, setValue] = usePreference(selectedKey)
+
+ const [inputValue, setInputValue] = useState('')
+
+ const handleSetValue = async () => {
+ try {
+ let parsedValue: any = inputValue
+
+ // Try to parse as JSON if it looks like an object/array/boolean/number
+ if (
+ inputValue.startsWith('{') ||
+ inputValue.startsWith('[') ||
+ inputValue === 'true' ||
+ inputValue === 'false' ||
+ !isNaN(Number(inputValue))
+ ) {
+ try {
+ parsedValue = JSON.parse(inputValue)
+ } catch {
+ // Keep as string if JSON parsing fails
+ }
+ }
+
+ await setValue(parsedValue)
+ message.success('设置成功')
+ setInputValue('')
+ } catch (error) {
+ message.error(`设置失败: ${(error as Error).message}`)
+ }
+ }
+
+ const testCases = [
+ { key: 'app.theme.mode', label: 'App Theme Mode', sampleValue: 'ThemeMode.dark' },
+ { key: 'app.language', label: 'App Language', sampleValue: 'zh-CN' },
+ { key: 'app.spell_check.enabled', label: 'Spell Check', sampleValue: 'true' },
+ { key: 'app.zoom_factor', label: 'Zoom Factor', sampleValue: '1.2' },
+ { key: 'app.tray.enabled', label: 'Tray Enabled', sampleValue: 'true' }
+ ]
+
+ return (
+
+
+ {/* Key Selection */}
+
+ 选择测试的偏好设置键:
+
+
+
+ {/* Current Value Display */}
+
+ 当前值:
+
+ {value !== undefined ? (
+ typeof value === 'object' ? (
+ JSON.stringify(value, null, 2)
+ ) : (
+ String(value)
+ )
+ ) : (
+ undefined (未设置或未加载)
+ )}
+
+
+
+ {/* Set New Value */}
+
+ 设置新值:
+
+ setInputValue(e.target.value)}
+ placeholder="输入新值 (JSON格式用于对象/数组)"
+ onPressEnter={handleSetValue}
+ />
+
+
+
+
+ {/* Quick Actions */}
+
+ 快速操作:
+
+ {/* Theme Toggle */}
+ {selectedKey === 'app.theme.mode' && (
+
+ )}
+
+ {/* Boolean Toggle */}
+ {selectedKey === 'app.spell_check.enabled' && (
+ setValue(checked)}
+ checkedChildren="启用"
+ unCheckedChildren="禁用"
+ />
+ )}
+
+ {/* Language Switch */}
+ {selectedKey === 'app.language' && (
+ <>
+
+
+ >
+ )}
+
+ {/* Zoom Factor */}
+ {selectedKey === 'app.zoom_factor' && (
+ <>
+
+
+
+ >
+ )}
+
+ {/* Sample Values */}
+
+
+
+
+ {/* Hook Status Info */}
+
+
+ Hook状态: 当前监听 "{selectedKey}", 值类型: {typeof value}, 是否已定义: {value !== undefined ? '是' : '否'}
+
+
+
+
+ )
+}
+
+const TestContainer = styled.div`
+ padding: 16px;
+ background: #fafafa;
+ border-radius: 8px;
+`
+
+const CurrentValueContainer = styled.div`
+ padding: 12px;
+ background: #f0f0f0;
+ border-radius: 6px;
+ border-left: 4px solid var(--color-primary);
+`
+
+const ValueDisplay = styled.pre`
+ margin: 8px 0 0 0;
+ font-family: 'Courier New', monospace;
+ font-size: 12px;
+ color: #333;
+ white-space: pre-wrap;
+ word-break: break-all;
+`
+
+const StatusContainer = styled.div`
+ padding: 8px;
+ background: #e6f7ff;
+ border-radius: 4px;
+ border: 1px solid #91d5ff;
+`
+
+export default PreferenceBasicTests
diff --git a/src/renderer/src/windows/dataRefactorTest/components/PreferenceHookTests.tsx b/src/renderer/src/windows/dataRefactorTest/components/PreferenceHookTests.tsx
new file mode 100644
index 0000000000..a7235a3235
--- /dev/null
+++ b/src/renderer/src/windows/dataRefactorTest/components/PreferenceHookTests.tsx
@@ -0,0 +1,177 @@
+import { usePreference, usePreferencePreload, usePreferenceService } from '@renderer/data/hooks/usePreference'
+import type { PreferenceKeyType } from '@shared/data/types'
+import { Button, Card, message, Space, Typography } from 'antd'
+import React, { useState } from 'react'
+import styled from 'styled-components'
+
+const { Text } = Typography
+
+/**
+ * Advanced usePreference hook testing component
+ * Tests preloading, service access, and hook behavior
+ */
+const PreferenceHookTests: React.FC = () => {
+ const preferenceService = usePreferenceService()
+ const [subscriptionCount, setSubscriptionCount] = useState(0)
+
+ // Test multiple hooks with same key
+ const [theme1] = usePreference('app.theme.mode')
+ const [theme2] = usePreference('app.theme.mode')
+ const [language] = usePreference('app.language')
+
+ // Preload test
+ usePreferencePreload(['app.theme.mode', 'app.language', 'app.zoom_factor'])
+
+ // Use useRef to track render count without causing re-renders
+ const renderCountRef = React.useRef(0)
+ renderCountRef.current += 1
+
+ const testSubscriptions = () => {
+ // Test subscription behavior
+ const unsubscribe = preferenceService.subscribeToKey('app.theme.mode')(() => {
+ setSubscriptionCount(prev => prev + 1)
+ })
+
+ message.info('已添加订阅,修改app.theme.mode将触发计数')
+
+ // Clean up after 10 seconds
+ setTimeout(() => {
+ unsubscribe()
+ message.info('订阅已取消')
+ }, 10000)
+ }
+
+ const testCacheWarming = async () => {
+ try {
+ const keys: PreferenceKeyType[] = ['app.theme.mode', 'app.language', 'app.zoom_factor', 'app.spell_check.enabled']
+ await preferenceService.preload(keys)
+
+ const cachedStates = keys.map(key => ({
+ key,
+ isCached: preferenceService.isCached(key),
+ value: preferenceService.getCachedValue(key)
+ }))
+
+ message.success(`预加载完成。缓存状态: ${cachedStates.filter(s => s.isCached).length}/${keys.length}`)
+ console.log('Cache states:', cachedStates)
+ } catch (error) {
+ message.error(`预加载失败: ${(error as Error).message}`)
+ }
+ }
+
+ const testBatchOperations = async () => {
+ try {
+ const keys: PreferenceKeyType[] = ['app.theme.mode', 'app.language']
+ const result = await preferenceService.getMultiple(keys)
+
+ message.success(`批量获取成功: ${Object.keys(result).length} 项`)
+ console.log('Batch get result:', result)
+
+ // Test batch set
+ await preferenceService.setMultiple({
+ 'app.theme.mode': theme1 === 'ThemeMode.dark' ? 'ThemeMode.light' : 'ThemeMode.dark',
+ 'app.language': language === 'zh-CN' ? 'en-US' : 'zh-CN'
+ })
+
+ message.success('批量设置成功')
+ } catch (error) {
+ message.error(`批量操作失败: ${(error as Error).message}`)
+ }
+ }
+
+ const performanceTest = async () => {
+ const start = performance.now()
+ const iterations = 100
+
+ try {
+ // Test rapid reads
+ for (let i = 0; i < iterations; i++) {
+ preferenceService.getCachedValue('app.theme.mode')
+ }
+
+ const readTime = performance.now() - start
+
+ // Test rapid writes
+ const writeStart = performance.now()
+ for (let i = 0; i < 10; i++) {
+ await preferenceService.set('app.theme.mode', `ThemeMode.test_${i}`)
+ }
+ const writeTime = performance.now() - writeStart
+
+ message.success(`性能测试完成: 读取${iterations}次耗时${readTime.toFixed(2)}ms, 写入10次耗时${writeTime.toFixed(2)}ms`)
+ } catch (error) {
+ message.error(`性能测试失败: ${(error as Error).message}`)
+ }
+ }
+
+ return (
+
+
+ {/* Hook State Display */}
+
+
+ 组件渲染次数: {renderCountRef.current}
+ 订阅触发次数: {subscriptionCount}
+ Theme Hook 1: {String(theme1)}
+ Theme Hook 2: {String(theme2)}
+ Language Hook: {String(language)}
+
+ 注意: 相同key的多个hook应该返回相同值
+
+
+
+
+ {/* Test Actions */}
+
+
+
+
+
+
+
+ {/* Service Information */}
+
+
+ Service实例: {preferenceService ? '已连接' : '未连接'}
+ 预加载Keys: app.theme.mode, app.language, app.zoom_factor
+
+ usePreferenceService() 返回的是同一个单例实例
+
+
+
+
+ {/* Hook Behavior Tests */}
+
+
+ 实时同步测试:
+
+ 1. 在其他测试组件中修改 app.theme.mode 或 app.language
+
+
+ 2. 观察此组件中的值是否实时更新
+
+
+ 3. 检查订阅触发次数是否增加
+
+
+
+
+
+ )
+}
+
+const TestContainer = styled.div`
+ padding: 16px;
+ background: #fafafa;
+ border-radius: 8px;
+`
+
+export default PreferenceHookTests
\ No newline at end of file
diff --git a/src/renderer/src/windows/dataRefactorTest/components/PreferenceMultipleTests.tsx b/src/renderer/src/windows/dataRefactorTest/components/PreferenceMultipleTests.tsx
new file mode 100644
index 0000000000..cb65eb6951
--- /dev/null
+++ b/src/renderer/src/windows/dataRefactorTest/components/PreferenceMultipleTests.tsx
@@ -0,0 +1,286 @@
+import { usePreferences } from '@renderer/data/hooks/usePreference'
+import { Button, Card, Input, message, Select, Space, Table, Typography } from 'antd'
+import { ColumnType } from 'antd/es/table'
+import React, { useState } from 'react'
+import styled from 'styled-components'
+
+const { Text } = Typography
+const { Option } = Select
+
+/**
+ * usePreferences hook testing component
+ * Tests multiple preferences management with batch operations
+ */
+const PreferenceMultipleTests: React.FC = () => {
+ // Define different test scenarios
+ const [scenario, setScenario] = useState('basic')
+
+ const scenarios = {
+ basic: {
+ theme: 'app.theme.mode',
+ language: 'app.language',
+ zoom: 'app.zoom_factor'
+ },
+ ui: {
+ theme: 'app.theme.mode',
+ zoom: 'app.zoom_factor',
+ spell: 'app.spell_check.enabled'
+ },
+ user: {
+ tray: 'app.tray.enabled',
+ userName: 'app.user.name',
+ devMode: 'app.developer_mode.enabled'
+ },
+ custom: {
+ key1: 'app.theme.mode',
+ key2: 'app.language',
+ key3: 'app.zoom_factor',
+ key4: 'app.spell_check.enabled'
+ }
+ } as const
+
+ const currentKeys = scenarios[scenario as keyof typeof scenarios]
+ const [values, updateValues] = usePreferences(currentKeys)
+
+ const [batchInput, setBatchInput] = useState('')
+
+ const handleBatchUpdate = async () => {
+ try {
+ const updates = JSON.parse(batchInput)
+ await updateValues(updates)
+ message.success('批量更新成功')
+ setBatchInput('')
+ } catch (error) {
+ if (error instanceof SyntaxError) {
+ message.error('JSON格式错误')
+ } else {
+ message.error(`更新失败: ${(error as Error).message}`)
+ }
+ }
+ }
+
+ const handleQuickUpdate = async (key: string, value: any) => {
+ try {
+ await updateValues({ [key]: value })
+ message.success(`${key} 更新成功`)
+ } catch (error) {
+ message.error(`更新失败: ${(error as Error).message}`)
+ }
+ }
+
+ const generateSampleBatch = () => {
+ const sampleUpdates: Record = {}
+ Object.keys(currentKeys).forEach((localKey, index) => {
+ const prefKey = currentKeys[localKey as keyof typeof currentKeys]
+
+ switch (prefKey) {
+ case 'app.theme.mode':
+ sampleUpdates[localKey] = values[localKey] === 'ThemeMode.dark' ? 'ThemeMode.light' : 'ThemeMode.dark'
+ break
+ case 'app.language':
+ sampleUpdates[localKey] = values[localKey] === 'zh-CN' ? 'en-US' : 'zh-CN'
+ break
+ case 'app.zoom_factor':
+ sampleUpdates[localKey] = 1.0 + (index * 0.1)
+ break
+ case 'app.spell_check.enabled':
+ sampleUpdates[localKey] = !values[localKey]
+ break
+ default:
+ sampleUpdates[localKey] = `sample_value_${index}`
+ }
+ })
+
+ setBatchInput(JSON.stringify(sampleUpdates, null, 2))
+ }
+
+ // Table columns for displaying values
+ const columns: ColumnType[] = [
+ {
+ title: '本地键名',
+ dataIndex: 'localKey',
+ key: 'localKey',
+ width: 120
+ },
+ {
+ title: '偏好设置键',
+ dataIndex: 'prefKey',
+ key: 'prefKey',
+ width: 200
+ },
+ {
+ title: '当前值',
+ dataIndex: 'value',
+ key: 'value',
+ render: (value: any) => (
+
+ {value !== undefined ? (
+ typeof value === 'object' ? JSON.stringify(value) : String(value)
+ ) : (
+ undefined
+ )}
+
+ )
+ },
+ {
+ title: '类型',
+ dataIndex: 'type',
+ key: 'type',
+ width: 80,
+ render: (type: string) => {type}
+ },
+ {
+ title: '操作',
+ key: 'actions',
+ width: 150,
+ render: (_, record) => (
+
+ {record.prefKey === 'app.theme.mode' && (
+
+ )}
+ {record.prefKey === 'app.spell_check.enabled' && (
+
+ )}
+ {record.prefKey === 'app.language' && (
+
+ )}
+
+ )
+ }
+ ]
+
+ // Transform data for table
+ const tableData = Object.keys(currentKeys).map((localKey, index) => ({
+ key: index,
+ localKey,
+ prefKey: currentKeys[localKey as keyof typeof currentKeys],
+ value: values[localKey],
+ type: typeof values[localKey]
+ }))
+
+ return (
+
+
+ {/* Scenario Selection */}
+
+
+ 选择测试场景:
+
+ 当前映射: {Object.keys(currentKeys).length} 项
+
+
+
+ {/* Current Values Table */}
+
+
+
+
+ {/* Batch Update */}
+
+
+
+
+
+
+
+ setBatchInput(e.target.value)}
+ placeholder="输入JSON格式的批量更新数据,例如: {"theme": "dark", "language": "en-US"}"
+ rows={6}
+ style={{ fontFamily: 'monospace' }}
+ />
+
+
+ 格式: {"localKey": "newValue", ...} - 使用本地键名,不是完整的偏好设置键名
+
+
+
+
+ {/* Quick Actions */}
+
+
+
+
+
+
+
+
+ {/* Hook Info */}
+
+
+
+ 本地键映射: {JSON.stringify(currentKeys, null, 2)}
+
+
+ 返回值数量: {Object.keys(values).length}
+
+
+ 已定义值: {Object.values(values).filter(v => v !== undefined).length}
+
+
+ usePreferences 返回的值对象使用本地键名,内部自动映射到实际的偏好设置键
+
+
+
+
+
+ )
+}
+
+const TestContainer = styled.div`
+ padding: 16px;
+ background: #fafafa;
+ border-radius: 8px;
+`
+
+const ValueDisplay = styled.span`
+ font-family: 'Courier New', monospace;
+ font-size: 12px;
+ word-break: break-all;
+`
+
+export default PreferenceMultipleTests
\ No newline at end of file
diff --git a/src/renderer/src/windows/dataRefactorTest/components/PreferenceServiceTests.tsx b/src/renderer/src/windows/dataRefactorTest/components/PreferenceServiceTests.tsx
new file mode 100644
index 0000000000..d21f7505f7
--- /dev/null
+++ b/src/renderer/src/windows/dataRefactorTest/components/PreferenceServiceTests.tsx
@@ -0,0 +1,218 @@
+import { preferenceService } from '@renderer/data/PreferenceService'
+import type { PreferenceKeyType } from '@shared/data/types'
+import { Button, Input, message, Space, Typography } from 'antd'
+import React, { useState } from 'react'
+import styled from 'styled-components'
+
+const { Text } = Typography
+
+/**
+ * PreferenceService direct API testing component
+ * Tests the service layer functionality without React hooks
+ */
+const PreferenceServiceTests: React.FC = () => {
+ const [testKey, setTestKey] = useState('app.theme.mode')
+ const [testValue, setTestValue] = useState('ThemeMode.dark')
+ const [getResult, setGetResult] = useState(null)
+ const [loading, setLoading] = useState(false)
+
+ const handleGet = async () => {
+ try {
+ setLoading(true)
+ const result = await preferenceService.get(testKey as PreferenceKeyType)
+ setGetResult(result)
+ message.success('获取成功')
+ } catch (error) {
+ message.error(`获取失败: ${(error as Error).message}`)
+ setGetResult(`Error: ${(error as Error).message}`)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleSet = async () => {
+ try {
+ setLoading(true)
+ let parsedValue: any = testValue
+
+ // Try to parse as JSON if it looks like an object/array/boolean/number
+ if (testValue.startsWith('{') || testValue.startsWith('[') || testValue === 'true' || testValue === 'false' || !isNaN(Number(testValue))) {
+ try {
+ parsedValue = JSON.parse(testValue)
+ } catch {
+ // Keep as string if JSON parsing fails
+ }
+ }
+
+ await preferenceService.set(testKey as PreferenceKeyType, parsedValue)
+ message.success('设置成功')
+ // Automatically get the updated value
+ await handleGet()
+ } catch (error) {
+ message.error(`设置失败: ${(error as Error).message}`)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleGetCached = () => {
+ try {
+ const result = preferenceService.getCachedValue(testKey as PreferenceKeyType)
+ setGetResult(result !== undefined ? result : 'undefined (not cached)')
+ message.info('获取缓存值成功')
+ } catch (error) {
+ message.error(`获取缓存失败: ${(error as Error).message}`)
+ setGetResult(`Error: ${(error as Error).message}`)
+ }
+ }
+
+ const handleIsCached = () => {
+ try {
+ const result = preferenceService.isCached(testKey as PreferenceKeyType)
+ setGetResult(result ? 'true (已缓存)' : 'false (未缓存)')
+ message.info('检查缓存状态成功')
+ } catch (error) {
+ message.error(`检查缓存失败: ${(error as Error).message}`)
+ }
+ }
+
+ const handlePreload = async () => {
+ try {
+ setLoading(true)
+ await preferenceService.preload([testKey as PreferenceKeyType])
+ message.success('预加载成功')
+ await handleGet()
+ } catch (error) {
+ message.error(`预加载失败: ${(error as Error).message}`)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleGetAll = async () => {
+ try {
+ setLoading(true)
+ // Get multiple keys to simulate getAll functionality
+ const sampleKeys = ['app.theme.mode', 'app.language', 'app.zoom_factor', 'app.spell_check.enabled', 'app.user.name'] as PreferenceKeyType[]
+ const result = await preferenceService.getMultiple(sampleKeys)
+ setGetResult(`Sample preferences (${Object.keys(result).length} keys):\n${JSON.stringify(result, null, 2)}`)
+ message.success('获取示例偏好设置成功')
+ } catch (error) {
+ message.error(`获取偏好设置失败: ${(error as Error).message}`)
+ setGetResult(`Error: ${(error as Error).message}`)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+ {/* Input Controls */}
+
+
+ Preference Key:
+ setTestKey(e.target.value)}
+ placeholder="Enter preference key (e.g., app.theme)"
+ style={{ marginTop: 4 }}
+ />
+
+
+ Value:
+ setTestValue(e.target.value)}
+ placeholder="Enter value (JSON format for objects/arrays)"
+ style={{ marginTop: 4 }}
+ />
+
+
+
+ {/* Action Buttons */}
+
+
+
+
+
+
+
+
+
+ {/* Result Display */}
+ {getResult !== null && (
+
+ Result:
+ {typeof getResult === 'object' ? JSON.stringify(getResult, null, 2) : String(getResult)}
+
+ )}
+
+ {/* Quick Test Buttons */}
+
+
+
+
+
+
+
+ )
+}
+
+const TestContainer = styled.div`
+ padding: 16px;
+ background: #fafafa;
+ border-radius: 8px;
+`
+
+const ResultContainer = styled.div`
+ margin-top: 16px;
+ padding: 12px;
+ background: #f0f0f0;
+ border-radius: 6px;
+ border-left: 4px solid var(--color-primary);
+`
+
+const ResultText = styled.pre`
+ margin: 8px 0 0 0;
+ font-family: 'Courier New', monospace;
+ font-size: 12px;
+ color: #333;
+ white-space: pre-wrap;
+ word-break: break-all;
+`
+
+export default PreferenceServiceTests
\ No newline at end of file
diff --git a/src/renderer/src/windows/dataRefactorTest/entryPoint.tsx b/src/renderer/src/windows/dataRefactorTest/entryPoint.tsx
new file mode 100644
index 0000000000..ac164184e8
--- /dev/null
+++ b/src/renderer/src/windows/dataRefactorTest/entryPoint.tsx
@@ -0,0 +1,13 @@
+import '@ant-design/v5-patch-for-react-19'
+import '@renderer/assets/styles/index.scss'
+
+import { loggerService } from '@logger'
+import { createRoot } from 'react-dom/client'
+
+import TestApp from './TestApp'
+
+loggerService.initWindowSource('DataRefactorTestWindow')
+
+const root = createRoot(document.getElementById('root') as HTMLElement)
+
+root.render()
\ No newline at end of file