mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 20:12:38 +08:00
refactor(Selector): enhance Selector component with option grouping, disabled state handling, and improved label retrieval logic
This commit is contained in:
parent
d9def89ced
commit
b5636646c9
@ -3,13 +3,23 @@ import { Check, ChevronsUpDown } from 'lucide-react'
|
||||
import { ReactNode, useMemo, useState } from 'react'
|
||||
import styled, { createGlobalStyle, css } from 'styled-components'
|
||||
|
||||
interface SelectorOption<V = string | number> {
|
||||
label: string | ReactNode
|
||||
value: V
|
||||
type?: 'group'
|
||||
options?: SelectorOption<V>[]
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
interface SelectorProps<V = string | number> {
|
||||
options: { label: string | ReactNode; value: V }[]
|
||||
options: SelectorOption<V>[]
|
||||
value?: V
|
||||
placeholder?: string
|
||||
placement?: 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight' | 'top' | 'bottom'
|
||||
/** 字体大小 */
|
||||
size?: number
|
||||
/** 是否禁用 */
|
||||
disabled?: boolean
|
||||
onChange: (value: V) => void
|
||||
}
|
||||
|
||||
@ -19,27 +29,47 @@ const Selector = <V extends string | number>({
|
||||
onChange = () => {},
|
||||
placement = 'bottomRight',
|
||||
size = 13,
|
||||
placeholder
|
||||
placeholder = '待选择',
|
||||
disabled = false
|
||||
}: SelectorProps<V>) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const label = useMemo(() => {
|
||||
if (value) {
|
||||
return options?.find((option) => option.value === value)?.label
|
||||
const findLabel = (opts: SelectorOption<V>[]): string | ReactNode | undefined => {
|
||||
for (const opt of opts) {
|
||||
if (opt.value === value) {
|
||||
return opt.label
|
||||
}
|
||||
if (opt.options) {
|
||||
const found = findLabel(opt.options)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
return findLabel(options) || placeholder
|
||||
}
|
||||
return placeholder
|
||||
}, [options, value, placeholder])
|
||||
|
||||
const items = useMemo(() => {
|
||||
return options.map((option) => ({
|
||||
const mapOption = (option: SelectorOption<V>) => ({
|
||||
key: option.value,
|
||||
label: option.label,
|
||||
extra: <CheckIcon>{option.value === value && <Check size={14} />}</CheckIcon>
|
||||
}))
|
||||
extra: <CheckIcon>{option.value === value && <Check size={14} />}</CheckIcon>,
|
||||
disabled: option.disabled,
|
||||
type: option.type || (option.options ? 'group' : undefined),
|
||||
children: option.options?.map(mapOption)
|
||||
})
|
||||
|
||||
return options.map(mapOption)
|
||||
}, [options, value])
|
||||
|
||||
function onClick(e: { key: string }) {
|
||||
onChange(e.key as V)
|
||||
if (!disabled) {
|
||||
onChange(e.key as V)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@ -55,11 +85,11 @@ const Selector = <V extends string | number>({
|
||||
<Dropdown
|
||||
overlayClassName="selector-dropdown"
|
||||
menu={{ items, onClick }}
|
||||
trigger={['click']}
|
||||
trigger={disabled ? [] : ['click']}
|
||||
placement={placement}
|
||||
open={open}
|
||||
onOpenChange={setOpen}>
|
||||
<Label $size={size} $open={open}>
|
||||
open={open && !disabled}
|
||||
onOpenChange={disabled ? undefined : setOpen}>
|
||||
<Label $size={size} $open={open} $disabled={disabled}>
|
||||
{label}
|
||||
<LabelIcon size={size + 3} />
|
||||
</Label>
|
||||
@ -82,23 +112,32 @@ const LabelIcon = styled(ChevronsUpDown)`
|
||||
transition: background-color 0.2s;
|
||||
`
|
||||
|
||||
const Label = styled.div<{ $size: number; $open: boolean }>`
|
||||
const Label = styled.div<{ $size: number; $open: boolean; $disabled: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border-radius: 99px;
|
||||
padding: 1px 2px 1px 10px;
|
||||
padding: 3px 2px 3px 10px;
|
||||
font-size: ${({ $size }) => $size}px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
line-height: 1;
|
||||
cursor: ${({ $disabled }) => ($disabled ? 'not-allowed' : 'pointer')};
|
||||
opacity: ${({ $disabled }) => ($disabled ? 0.6 : 1)};
|
||||
transition:
|
||||
background-color 0.2s,
|
||||
opacity 0.2s;
|
||||
&:hover {
|
||||
background-color: var(--color-background-mute);
|
||||
${LabelIcon} {
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
${({ $disabled }) =>
|
||||
!$disabled &&
|
||||
css`
|
||||
background-color: var(--color-background-mute);
|
||||
${LabelIcon} {
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
`}
|
||||
}
|
||||
${({ $open }) =>
|
||||
${({ $open, $disabled }) =>
|
||||
$open &&
|
||||
!$disabled &&
|
||||
css`
|
||||
background-color: var(--color-background-mute);
|
||||
${LabelIcon} {
|
||||
|
||||
@ -913,6 +913,7 @@ const MainContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
background-color: var(--color-background);
|
||||
`
|
||||
@ -920,6 +921,7 @@ const MainContainer = styled.div`
|
||||
const InputContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
min-height: 95px;
|
||||
max-height: 95px;
|
||||
position: relative;
|
||||
|
||||
@ -859,6 +859,7 @@ const MainContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
background-color: var(--color-background);
|
||||
`
|
||||
@ -866,6 +867,7 @@ const MainContainer = styled.div`
|
||||
const InputContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
min-height: 95px;
|
||||
max-height: 95px;
|
||||
position: relative;
|
||||
@ -977,7 +979,8 @@ const ModeSegmentedContainer = styled.div`
|
||||
|
||||
const EmptyImgBox = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: 70vh;
|
||||
height: 100%;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@ -985,7 +988,7 @@ const EmptyImgBox = styled.div`
|
||||
|
||||
const EmptyImg = styled.div<{ bgUrl?: string }>`
|
||||
width: 70vh;
|
||||
height: 70vh;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
background-image: ${(props) => (props.bgUrl ? `url(${props.bgUrl})` : `url(${DMXAPIToImg})`)};
|
||||
`
|
||||
|
||||
@ -560,6 +560,7 @@ const MainContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
background-color: var(--color-background);
|
||||
`
|
||||
|
||||
@ -666,6 +666,7 @@ const MainContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
background-color: var(--color-background);
|
||||
`
|
||||
|
||||
@ -104,6 +104,7 @@ const Artboard: FC<ArtboardProps> = ({
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
@ -114,11 +115,10 @@ const Container = styled.div`
|
||||
const ImagePlaceholder = styled.div`
|
||||
display: flex;
|
||||
width: 70vh;
|
||||
height: 70vh;
|
||||
height: 100%;
|
||||
background-color: var(--color-background-soft);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
`
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user