mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-03 11:19:10 +08:00
feat(preferences): implement optimistic and pessimistic update strategies in PreferenceService
- Introduced `PreferenceUpdateOptions` interface to configure update behavior. - Enhanced `set` and `setMultiple` methods in `PreferenceService` to support both optimistic and pessimistic update strategies, allowing for immediate UI feedback or database-first updates. - Updated `usePreference` and `useMultiplePreferences` hooks to accept options for flexible update strategies. - Improved handling of concurrent updates with request queues and rollback mechanisms for optimistic updates. - Enhanced documentation in hooks to clarify usage of update strategies.
This commit is contained in:
parent
e15005d1cf
commit
9bde833419
4
packages/shared/data/types.d.ts
vendored
4
packages/shared/data/types.d.ts
vendored
@ -9,3 +9,7 @@ export type PreferenceShortcutType = {
|
||||
enabled: boolean
|
||||
system: boolean
|
||||
}
|
||||
|
||||
export interface PreferenceUpdateOptions {
|
||||
optimistic: boolean
|
||||
}
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { DefaultPreferences } from '@shared/data/preferences'
|
||||
import type { PreferenceDefaultScopeType, PreferenceKeyType } from '@shared/data/types'
|
||||
import type { PreferenceDefaultScopeType, PreferenceKeyType, PreferenceUpdateOptions } from '@shared/data/types'
|
||||
|
||||
const logger = loggerService.withContext('PreferenceService')
|
||||
|
||||
/**
|
||||
* Renderer-side PreferenceService providing cached access to preferences
|
||||
* with real-time synchronization across windows using useSyncExternalStore
|
||||
@ -21,6 +20,29 @@ export class PreferenceService {
|
||||
|
||||
private fullCacheLoaded = false
|
||||
|
||||
// Optimistic update tracking
|
||||
private optimisticValues = new Map<
|
||||
PreferenceKeyType,
|
||||
{
|
||||
value: any
|
||||
originalValue: any
|
||||
timestamp: number
|
||||
requestId: string
|
||||
isFirst: boolean
|
||||
}
|
||||
>()
|
||||
|
||||
// Request queues for managing concurrent updates to the same key
|
||||
private requestQueues = new Map<
|
||||
PreferenceKeyType,
|
||||
Array<{
|
||||
requestId: string
|
||||
value: any
|
||||
resolve: (value: void | PromiseLike<void>) => void
|
||||
reject: (reason?: any) => void
|
||||
}>
|
||||
>()
|
||||
|
||||
private constructor() {
|
||||
this.setupChangeListeners()
|
||||
}
|
||||
@ -97,19 +119,85 @@ export class PreferenceService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a single preference value
|
||||
* Set a single preference value with configurable update strategy
|
||||
*/
|
||||
public async set<K extends PreferenceKeyType>(key: K, value: PreferenceDefaultScopeType[K]): Promise<void> {
|
||||
public async set<K extends PreferenceKeyType>(
|
||||
key: K,
|
||||
value: PreferenceDefaultScopeType[K],
|
||||
options: PreferenceUpdateOptions = { optimistic: true }
|
||||
): Promise<void> {
|
||||
if (options.optimistic) {
|
||||
return this.setOptimistic(key, value)
|
||||
} else {
|
||||
return this.setPessimistic(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimistic update: Queue request to prevent race conditions
|
||||
*/
|
||||
private async setOptimistic<K extends PreferenceKeyType>(
|
||||
key: K,
|
||||
value: PreferenceDefaultScopeType[K]
|
||||
): Promise<void> {
|
||||
const requestId = this.generateRequestId()
|
||||
return this.enqueueRequest(key, requestId, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute optimistic update with proper race condition handling
|
||||
*/
|
||||
private async executeOptimisticUpdate(key: PreferenceKeyType, value: any, requestId: string): Promise<void> {
|
||||
const existingState = this.optimisticValues.get(key)
|
||||
const isFirst = !existingState
|
||||
const originalValue = isFirst ? this.cache[key] : existingState.originalValue
|
||||
|
||||
// Update cache immediately for responsive UI
|
||||
this.cache[key] = value
|
||||
this.notifyChangeListeners(key)
|
||||
|
||||
// Track optimistic state with proper original value protection
|
||||
this.optimisticValues.set(key, {
|
||||
value,
|
||||
originalValue, // Use real original value (from first request) or current if first
|
||||
timestamp: Date.now(),
|
||||
requestId,
|
||||
isFirst
|
||||
})
|
||||
|
||||
logger.debug(`Optimistic update for ${key} (${requestId})${isFirst ? ' [FIRST]' : ''}`)
|
||||
|
||||
// Attempt to persist to main process
|
||||
try {
|
||||
await window.api.preference.set(key, value)
|
||||
// Success: confirm optimistic update
|
||||
this.confirmOptimistic(key, requestId)
|
||||
logger.debug(`Optimistic update for ${key} (${requestId}) confirmed`)
|
||||
} catch (error) {
|
||||
// Failure: rollback optimistic update
|
||||
this.rollbackOptimistic(key, requestId)
|
||||
logger.error(`Optimistic update failed for ${key} (${requestId}), rolling back:`, error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pessimistic update: Wait for database confirmation before updating UI
|
||||
*/
|
||||
private async setPessimistic<K extends PreferenceKeyType>(
|
||||
key: K,
|
||||
value: PreferenceDefaultScopeType[K]
|
||||
): Promise<void> {
|
||||
try {
|
||||
await window.api.preference.set(key, value)
|
||||
|
||||
// Update local cache immediately for responsive UI
|
||||
// Update local cache after successful database update
|
||||
this.cache[key] = value
|
||||
this.notifyChangeListeners(key)
|
||||
|
||||
logger.debug(`Preference ${key} set to:`, { value })
|
||||
logger.debug(`Pessimistic update for ${key} completed`)
|
||||
} catch (error) {
|
||||
logger.error(`Failed to set preference ${key}:`, error as Error)
|
||||
logger.error(`Pessimistic update failed for ${key}:`, error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@ -166,21 +254,90 @@ export class PreferenceService {
|
||||
return cachedResults
|
||||
}
|
||||
/**
|
||||
* Set multiple preferences at once
|
||||
* Set multiple preferences at once with configurable update strategy
|
||||
*/
|
||||
public async setMultiple(updates: Partial<PreferenceDefaultScopeType>): Promise<void> {
|
||||
public async setMultiple(
|
||||
updates: Partial<PreferenceDefaultScopeType>,
|
||||
options: PreferenceUpdateOptions = { optimistic: true }
|
||||
): Promise<void> {
|
||||
if (options.optimistic) {
|
||||
return this.setMultipleOptimistic(updates)
|
||||
} else {
|
||||
return this.setMultiplePessimistic(updates)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimistic batch update: Update UI immediately, then sync to database
|
||||
*/
|
||||
private async setMultipleOptimistic(updates: Partial<PreferenceDefaultScopeType>): Promise<void> {
|
||||
const batchRequestId = this.generateRequestId()
|
||||
const originalValues: Record<string, any> = {}
|
||||
const keysToUpdate = Object.keys(updates) as PreferenceKeyType[]
|
||||
|
||||
// For batch updates, we need to check for existing optimistic states
|
||||
// and preserve the original values from first requests
|
||||
for (const key of keysToUpdate) {
|
||||
const existingState = this.optimisticValues.get(key)
|
||||
originalValues[key] = existingState ? existingState.originalValue : this.cache[key as PreferenceKeyType]
|
||||
}
|
||||
|
||||
// Update cache immediately and track original values
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
this.cache[key as PreferenceKeyType] = value
|
||||
this.notifyChangeListeners(key)
|
||||
}
|
||||
|
||||
// Track optimistic states for all keys with proper original value protection
|
||||
const timestamp = Date.now()
|
||||
keysToUpdate.forEach((key) => {
|
||||
const existingState = this.optimisticValues.get(key)
|
||||
const isFirst = !existingState
|
||||
|
||||
this.optimisticValues.set(key, {
|
||||
value: updates[key as PreferenceKeyType],
|
||||
originalValue: originalValues[key], // Use protected original value
|
||||
timestamp,
|
||||
requestId: `${batchRequestId}_${key}`, // Unique ID per key in batch
|
||||
isFirst
|
||||
})
|
||||
})
|
||||
|
||||
logger.debug(`Optimistic batch update for ${keysToUpdate.length} preferences (${batchRequestId})`)
|
||||
|
||||
// Attempt to persist to main process
|
||||
try {
|
||||
await window.api.preference.setMultiple(updates)
|
||||
// Success: confirm all optimistic updates
|
||||
keysToUpdate.forEach((key) => this.confirmOptimistic(key, `${batchRequestId}_${key}`))
|
||||
logger.debug(`Optimistic batch update confirmed for ${keysToUpdate.length} preferences (${batchRequestId})`)
|
||||
} catch (error) {
|
||||
// Failure: rollback all optimistic updates
|
||||
keysToUpdate.forEach((key) => this.rollbackOptimistic(key, `${batchRequestId}_${key}`))
|
||||
logger.error(
|
||||
`Optimistic batch update failed, rolling back ${keysToUpdate.length} preferences (${batchRequestId}):`,
|
||||
error as Error
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pessimistic batch update: Wait for database confirmation before updating UI
|
||||
*/
|
||||
private async setMultiplePessimistic(updates: Partial<PreferenceDefaultScopeType>): Promise<void> {
|
||||
try {
|
||||
await window.api.preference.setMultiple(updates)
|
||||
|
||||
// Update local cache for all updated values
|
||||
// Update local cache for all updated values after successful database update
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
this.cache[key as PreferenceKeyType] = value
|
||||
this.notifyChangeListeners(key)
|
||||
}
|
||||
|
||||
logger.debug(`Updated ${Object.keys(updates).length} preferences`)
|
||||
logger.debug(`Pessimistic batch update completed for ${Object.keys(updates).length} preferences`)
|
||||
} catch (error) {
|
||||
logger.error('Failed to set multiple preferences:', error as Error)
|
||||
logger.error(`Pessimistic batch update failed:`, error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@ -299,6 +456,132 @@ export class PreferenceService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm an optimistic update (called when main process confirms the update)
|
||||
*/
|
||||
private confirmOptimistic(key: PreferenceKeyType, requestId: string): void {
|
||||
const optimisticState = this.optimisticValues.get(key)
|
||||
if (optimisticState && optimisticState.requestId === requestId) {
|
||||
this.optimisticValues.delete(key)
|
||||
logger.debug(`Optimistic update confirmed for ${key} (${requestId})`)
|
||||
|
||||
// Process next queued request
|
||||
this.completeQueuedRequest(key)
|
||||
} else {
|
||||
logger.warn(
|
||||
`Attempted to confirm mismatched request for ${key}: expected ${optimisticState?.requestId}, got ${requestId}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback an optimistic update (called on failure)
|
||||
*/
|
||||
private rollbackOptimistic(key: PreferenceKeyType, requestId: string): void {
|
||||
const optimisticState = this.optimisticValues.get(key)
|
||||
if (optimisticState && optimisticState.requestId === requestId) {
|
||||
// Restore original value (the real original value from first request)
|
||||
this.cache[key] = optimisticState.originalValue
|
||||
this.notifyChangeListeners(key)
|
||||
|
||||
// Clear optimistic state
|
||||
this.optimisticValues.delete(key)
|
||||
|
||||
const duration = Date.now() - optimisticState.timestamp
|
||||
logger.warn(`Optimistic update rolled back for ${key} (${requestId}) after ${duration}ms to original value`)
|
||||
|
||||
// Process next queued request
|
||||
this.completeQueuedRequest(key)
|
||||
} else {
|
||||
logger.warn(
|
||||
`Attempted to rollback mismatched request for ${key}: expected ${optimisticState?.requestId}, got ${requestId}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pending optimistic updates (for debugging)
|
||||
*/
|
||||
public getPendingOptimisticUpdates(): Array<{
|
||||
key: string
|
||||
value: any
|
||||
originalValue: any
|
||||
timestamp: number
|
||||
requestId: string
|
||||
isFirst: boolean
|
||||
}> {
|
||||
return Array.from(this.optimisticValues.entries()).map(([key, state]) => ({
|
||||
key,
|
||||
value: state.value,
|
||||
originalValue: state.originalValue,
|
||||
timestamp: state.timestamp,
|
||||
requestId: state.requestId,
|
||||
isFirst: state.isFirst
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique request ID for tracking concurrent requests
|
||||
*/
|
||||
private generateRequestId(): string {
|
||||
return `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Add request to queue for a specific key
|
||||
*/
|
||||
private enqueueRequest(key: PreferenceKeyType, requestId: string, value: any): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!this.requestQueues.has(key)) {
|
||||
this.requestQueues.set(key, [])
|
||||
}
|
||||
|
||||
const queue = this.requestQueues.get(key)!
|
||||
queue.push({ requestId, value, resolve, reject })
|
||||
|
||||
// If this is the first request in queue, process it immediately
|
||||
if (queue.length === 1) {
|
||||
this.processNextQueuedRequest(key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the next queued request for a key
|
||||
*/
|
||||
private async processNextQueuedRequest(key: PreferenceKeyType): Promise<void> {
|
||||
const queue = this.requestQueues.get(key)
|
||||
if (!queue || queue.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentRequest = queue[0]
|
||||
try {
|
||||
await this.executeOptimisticUpdate(key, currentRequest.value, currentRequest.requestId)
|
||||
currentRequest.resolve()
|
||||
} catch (error) {
|
||||
currentRequest.reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete current request and process next in queue
|
||||
*/
|
||||
private completeQueuedRequest(key: PreferenceKeyType): void {
|
||||
const queue = this.requestQueues.get(key)
|
||||
if (queue && queue.length > 0) {
|
||||
queue.shift() // Remove completed request
|
||||
|
||||
// Process next request if any
|
||||
if (queue.length > 0) {
|
||||
this.processNextQueuedRequest(key)
|
||||
} else {
|
||||
// Clean up empty queue
|
||||
this.requestQueues.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached preferences (for testing/debugging)
|
||||
*/
|
||||
@ -316,6 +599,11 @@ export class PreferenceService {
|
||||
this.changeListenerCleanup()
|
||||
this.changeListenerCleanup = null
|
||||
}
|
||||
|
||||
// Clear all optimistic states and request queues
|
||||
this.optimisticValues.clear()
|
||||
this.requestQueues.clear()
|
||||
|
||||
this.clearCache()
|
||||
this.allChangesListeners.clear()
|
||||
this.keyChangeListeners.clear()
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { preferenceService } from '@data/PreferenceService'
|
||||
import { loggerService } from '@logger'
|
||||
import type { PreferenceDefaultScopeType, PreferenceKeyType } from '@shared/data/types'
|
||||
import type { PreferenceDefaultScopeType, PreferenceKeyType, PreferenceUpdateOptions } from '@shared/data/types'
|
||||
import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'
|
||||
|
||||
const logger = loggerService.withContext('usePreference')
|
||||
@ -8,17 +8,28 @@ const logger = loggerService.withContext('usePreference')
|
||||
/**
|
||||
* React hook for managing a single preference value with automatic synchronization
|
||||
* Uses useSyncExternalStore for optimal React 18 integration and real-time updates
|
||||
* Supports both optimistic and pessimistic update strategies for flexible UX
|
||||
*
|
||||
* @param key - The preference key to manage (must be a valid PreferenceKeyType)
|
||||
* @param options - Optional configuration for update behavior:
|
||||
* - strategy: 'optimistic' (default) for immediate UI updates, 'pessimistic' for database-first updates
|
||||
* @returns A tuple [value, setValue] where:
|
||||
* - value: Current preference value or undefined if not loaded/cached
|
||||
* - setValue: Async function to update the preference value
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Basic usage - managing theme preference
|
||||
* // Basic usage - managing theme preference with optimistic updates (default)
|
||||
* const [theme, setTheme] = usePreference('app.theme.mode')
|
||||
*
|
||||
* // Pessimistic updates for critical settings
|
||||
* const [apiKey, setApiKey] = usePreference('api.key', { strategy: 'pessimistic' })
|
||||
*
|
||||
* // Simple optimistic updates
|
||||
* const [fontSize, setFontSize] = usePreference('chat.message.font_size', {
|
||||
* strategy: 'optimistic'
|
||||
* })
|
||||
*
|
||||
* // Conditional rendering based on preference value
|
||||
* if (theme === undefined) {
|
||||
* return <LoadingSpinner />
|
||||
@ -27,9 +38,9 @@ const logger = loggerService.withContext('usePreference')
|
||||
* // Updating preference value
|
||||
* const handleThemeChange = async (newTheme: string) => {
|
||||
* try {
|
||||
* await setTheme(newTheme)
|
||||
* await setTheme(newTheme) // UI updates immediately with optimistic strategy
|
||||
* } catch (error) {
|
||||
* console.error('Failed to update theme:', error)
|
||||
* console.error('Failed to update theme:', error) // Will auto-rollback on failure
|
||||
* }
|
||||
* }
|
||||
*
|
||||
@ -45,13 +56,15 @@ const logger = loggerService.withContext('usePreference')
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Advanced usage with form handling for message font size
|
||||
* const [fontSize, setFontSize] = usePreference('chat.message.font_size')
|
||||
* const [fontSize, setFontSize] = usePreference('chat.message.font_size', {
|
||||
* strategy: 'optimistic' // Immediate feedback for UI preferences
|
||||
* })
|
||||
*
|
||||
* const handleFontSizeChange = useCallback(async (size: number) => {
|
||||
* if (size < 8 || size > 72) {
|
||||
* throw new Error('Font size must be between 8 and 72')
|
||||
* }
|
||||
* await setFontSize(size)
|
||||
* await setFontSize(size) // Immediate UI update, syncs to database
|
||||
* }, [setFontSize])
|
||||
*
|
||||
* return (
|
||||
@ -66,7 +79,8 @@ const logger = loggerService.withContext('usePreference')
|
||||
* ```
|
||||
*/
|
||||
export function usePreference<K extends PreferenceKeyType>(
|
||||
key: K
|
||||
key: K,
|
||||
options: PreferenceUpdateOptions = { optimistic: true }
|
||||
): [PreferenceDefaultScopeType[K] | undefined, (value: PreferenceDefaultScopeType[K]) => Promise<void>] {
|
||||
// Subscribe to changes for this specific preference
|
||||
const value = useSyncExternalStore(
|
||||
@ -88,13 +102,13 @@ export function usePreference<K extends PreferenceKeyType>(
|
||||
const setValue = useCallback(
|
||||
async (newValue: PreferenceDefaultScopeType[K]) => {
|
||||
try {
|
||||
await preferenceService.set(key, newValue)
|
||||
await preferenceService.set(key, newValue, options)
|
||||
} catch (error) {
|
||||
logger.error(`Failed to set preference ${key}:`, error as Error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
[key]
|
||||
[key, options]
|
||||
)
|
||||
|
||||
return [value, setValue]
|
||||
@ -103,22 +117,31 @@ export function usePreference<K extends PreferenceKeyType>(
|
||||
/**
|
||||
* React hook for managing multiple preference values with efficient batch operations
|
||||
* Automatically synchronizes all specified preferences and provides type-safe access
|
||||
* Supports both optimistic and pessimistic update strategies for flexible UX
|
||||
*
|
||||
* @param keys - Object mapping local names to preference keys. Keys are your custom names,
|
||||
* values must be valid PreferenceKeyType identifiers
|
||||
* @param options - Optional configuration for update behavior:
|
||||
* - strategy: 'optimistic' (default) for immediate UI updates, 'pessimistic' for database-first updates
|
||||
* @returns A tuple [values, updateValues] where:
|
||||
* - values: Object with your local keys mapped to current preference values (undefined if not loaded)
|
||||
* - updateValues: Async function to batch update multiple preferences at once
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Basic usage - managing related UI preferences
|
||||
* // Basic usage - managing related UI preferences with optimistic updates
|
||||
* const [uiSettings, setUISettings] = useMultiplePreferences({
|
||||
* theme: 'app.theme.mode',
|
||||
* fontSize: 'chat.message.font_size',
|
||||
* showLineNumbers: 'chat.code.show_line_numbers'
|
||||
* })
|
||||
*
|
||||
* // Pessimistic updates for critical settings
|
||||
* const [apiSettings, setApiSettings] = useMultiplePreferences({
|
||||
* apiKey: 'api.key',
|
||||
* endpoint: 'api.endpoint'
|
||||
* }, { strategy: 'pessimistic' })
|
||||
*
|
||||
* // Accessing individual values with type safety
|
||||
* const currentTheme = uiSettings.theme // string | undefined
|
||||
* const currentFontSize = uiSettings.fontSize // number | undefined
|
||||
@ -221,7 +244,8 @@ export function usePreference<K extends PreferenceKeyType>(
|
||||
* ```
|
||||
*/
|
||||
export function useMultiplePreferences<T extends Record<string, PreferenceKeyType>>(
|
||||
keys: T
|
||||
keys: T,
|
||||
options: PreferenceUpdateOptions = { optimistic: true }
|
||||
): [
|
||||
{ [P in keyof T]: PreferenceDefaultScopeType[T[P]] | undefined },
|
||||
(updates: Partial<{ [P in keyof T]: PreferenceDefaultScopeType[T[P]] }>) => Promise<void>
|
||||
@ -294,13 +318,13 @@ export function useMultiplePreferences<T extends Record<string, PreferenceKeyTyp
|
||||
}
|
||||
}
|
||||
|
||||
await preferenceService.setMultiple(prefUpdates)
|
||||
await preferenceService.setMultiple(prefUpdates, options)
|
||||
} catch (error) {
|
||||
logger.error('Failed to update preferences:', error as Error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
[keys]
|
||||
[keys, options]
|
||||
)
|
||||
|
||||
// Type-cast the values to the expected shape
|
||||
|
||||
@ -220,7 +220,11 @@ const PreferenceMultipleTests: React.FC = () => {
|
||||
{/* Zoom Factor Slider */}
|
||||
<div>
|
||||
<Text strong>
|
||||
缩放因子: {Math.round((typeof (values as any).zoomFactor === 'number' ? (values as any).zoomFactor : 1.0) * 100)}%
|
||||
缩放因子:{' '}
|
||||
{Math.round(
|
||||
(typeof (values as any).zoomFactor === 'number' ? (values as any).zoomFactor : 1.0) * 100
|
||||
)}
|
||||
%
|
||||
</Text>
|
||||
<Slider
|
||||
min={0.5}
|
||||
@ -240,7 +244,9 @@ const PreferenceMultipleTests: React.FC = () => {
|
||||
|
||||
{/* Font Size Slider */}
|
||||
<div>
|
||||
<Text strong>字体大小: {typeof (values as any).fontSize === 'number' ? (values as any).fontSize : 14}px</Text>
|
||||
<Text strong>
|
||||
字体大小: {typeof (values as any).fontSize === 'number' ? (values as any).fontSize : 14}px
|
||||
</Text>
|
||||
<Slider
|
||||
min={8}
|
||||
max={72}
|
||||
@ -262,7 +268,8 @@ const PreferenceMultipleTests: React.FC = () => {
|
||||
{/* Selection Window Opacity Slider */}
|
||||
<div>
|
||||
<Text strong>
|
||||
选择窗口透明度: {Math.round(typeof (values as any).opacity === 'number' ? (values as any).opacity : 100)}%
|
||||
选择窗口透明度:{' '}
|
||||
{Math.round(typeof (values as any).opacity === 'number' ? (values as any).opacity : 100)}%
|
||||
</Text>
|
||||
<Slider
|
||||
min={10}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user