mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 21:28:25 +08:00
feat(web): add resizable sidebar to skill page with localStorage persistence
This commit is contained in:
parent
190453d397
commit
a0526143e2
@ -27,3 +27,7 @@ export const NODE_MENU_TYPE = {
|
||||
} as const
|
||||
|
||||
export type NodeMenuType = (typeof NODE_MENU_TYPE)[keyof typeof NODE_MENU_TYPE]
|
||||
|
||||
export const SIDEBAR_MIN_WIDTH = 240
|
||||
export const SIDEBAR_MAX_WIDTH = 480
|
||||
export const SIDEBAR_DEFAULT_WIDTH = 320
|
||||
|
||||
@ -1,12 +1,47 @@
|
||||
'use client'
|
||||
|
||||
import type { FC, PropsWithChildren } from 'react'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { STORAGE_KEYS } from '@/config/storage-keys'
|
||||
import { storage } from '@/utils/storage'
|
||||
import { useResizePanel } from '../nodes/_base/hooks/use-resize-panel'
|
||||
import { SIDEBAR_DEFAULT_WIDTH, SIDEBAR_MAX_WIDTH, SIDEBAR_MIN_WIDTH } from './constants'
|
||||
|
||||
type SidebarProps = PropsWithChildren
|
||||
|
||||
const Sidebar: FC<SidebarProps> = ({ children }) => {
|
||||
const { run: persistWidth } = useDebounceFn(
|
||||
(width: number) => storage.set(STORAGE_KEYS.SKILL.SIDEBAR_WIDTH, width),
|
||||
{ wait: 200 },
|
||||
)
|
||||
|
||||
const handleResize = useCallback((width: number) => {
|
||||
persistWidth(width)
|
||||
}, [persistWidth])
|
||||
|
||||
const { triggerRef, containerRef } = useResizePanel({
|
||||
direction: 'horizontal',
|
||||
triggerDirection: 'right',
|
||||
minWidth: SIDEBAR_MIN_WIDTH,
|
||||
maxWidth: SIDEBAR_MAX_WIDTH,
|
||||
onResize: handleResize,
|
||||
})
|
||||
|
||||
return (
|
||||
<aside className="flex h-full w-[320px] shrink-0 flex-col gap-px overflow-hidden rounded-[10px] border border-components-panel-border-subtle bg-components-panel-bg">
|
||||
<aside
|
||||
ref={containerRef}
|
||||
style={{ width: storage.getNumber(STORAGE_KEYS.SKILL.SIDEBAR_WIDTH, SIDEBAR_DEFAULT_WIDTH) }}
|
||||
className="relative flex h-full shrink-0 flex-col gap-px overflow-hidden rounded-[10px] border border-components-panel-border-subtle bg-components-panel-bg"
|
||||
>
|
||||
{children}
|
||||
<div
|
||||
ref={triggerRef}
|
||||
className="absolute -right-1 top-0 z-10 flex h-full w-2 cursor-col-resize items-center justify-center"
|
||||
>
|
||||
<div className="h-10 w-0.5 rounded-sm bg-state-base-handle transition-all hover:h-full hover:bg-state-accent-solid active:h-full active:bg-state-accent-solid" />
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
5
web/config/storage-keys.ts
Normal file
5
web/config/storage-keys.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export const STORAGE_KEYS = {
|
||||
SKILL: {
|
||||
SIDEBAR_WIDTH: 'skill-sidebar-width',
|
||||
},
|
||||
} as const
|
||||
117
web/utils/storage.ts
Normal file
117
web/utils/storage.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import { isClient } from './client'
|
||||
|
||||
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }
|
||||
|
||||
const STORAGE_VERSION = 'v1'
|
||||
|
||||
let _isAvailable: boolean | null = null
|
||||
|
||||
function isLocalStorageAvailable(): boolean {
|
||||
if (_isAvailable !== null)
|
||||
return _isAvailable
|
||||
|
||||
if (!isClient) {
|
||||
_isAvailable = false
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const testKey = '__storage_test__'
|
||||
localStorage.setItem(testKey, 'test')
|
||||
localStorage.removeItem(testKey)
|
||||
_isAvailable = true
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
_isAvailable = false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function versionedKey(key: string): string {
|
||||
return `${STORAGE_VERSION}:${key}`
|
||||
}
|
||||
|
||||
function get<T extends JsonValue>(key: string, defaultValue?: T): T | null {
|
||||
if (!isLocalStorageAvailable())
|
||||
return defaultValue ?? null
|
||||
|
||||
try {
|
||||
const item = localStorage.getItem(versionedKey(key))
|
||||
if (item === null)
|
||||
return defaultValue ?? null
|
||||
|
||||
try {
|
||||
return JSON.parse(item) as T
|
||||
}
|
||||
catch {
|
||||
return item as T
|
||||
}
|
||||
}
|
||||
catch {
|
||||
return defaultValue ?? null
|
||||
}
|
||||
}
|
||||
|
||||
function set<T extends JsonValue>(key: string, value: T): void {
|
||||
if (!isLocalStorageAvailable())
|
||||
return
|
||||
|
||||
try {
|
||||
const stringValue = typeof value === 'string' ? value : JSON.stringify(value)
|
||||
localStorage.setItem(versionedKey(key), stringValue)
|
||||
}
|
||||
catch {
|
||||
// Silent fail - localStorage may be full or disabled
|
||||
}
|
||||
}
|
||||
|
||||
function remove(key: string): void {
|
||||
if (!isLocalStorageAvailable())
|
||||
return
|
||||
|
||||
try {
|
||||
localStorage.removeItem(versionedKey(key))
|
||||
}
|
||||
catch {
|
||||
// Silent fail
|
||||
}
|
||||
}
|
||||
|
||||
function getNumber(key: string): number | null
|
||||
function getNumber(key: string, defaultValue: number): number
|
||||
function getNumber(key: string, defaultValue?: number): number | null {
|
||||
const value = get<string | number>(key)
|
||||
if (value === null)
|
||||
return defaultValue ?? null
|
||||
|
||||
const parsed = typeof value === 'number' ? value : Number.parseFloat(value as string)
|
||||
return Number.isNaN(parsed) ? (defaultValue ?? null) : parsed
|
||||
}
|
||||
|
||||
function getBoolean(key: string): boolean | null
|
||||
function getBoolean(key: string, defaultValue: boolean): boolean
|
||||
function getBoolean(key: string, defaultValue?: boolean): boolean | null {
|
||||
const value = get<string | boolean>(key)
|
||||
if (value === null)
|
||||
return defaultValue ?? null
|
||||
|
||||
if (typeof value === 'boolean')
|
||||
return value
|
||||
|
||||
return value === 'true'
|
||||
}
|
||||
|
||||
function resetCache(): void {
|
||||
_isAvailable = null
|
||||
}
|
||||
|
||||
export const storage = {
|
||||
get,
|
||||
set,
|
||||
remove,
|
||||
getNumber,
|
||||
getBoolean,
|
||||
isAvailable: isLocalStorageAvailable,
|
||||
resetCache,
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user