Skip to content

Commit ba71a20

Browse files
committed
feat: enhance level filter
1 parent 63628fd commit ba71a20

File tree

4 files changed

+154
-15
lines changed

4 files changed

+154
-15
lines changed

src/apis/level.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ export const useLevels = ({ suspense }: { suspense?: boolean } = {}) => {
2828
}
2929

3030
if (stageIds.has(level.stageId)) {
31-
console.warn('Duplicate level removed:', level.stageId, level.name)
3231
return false
3332
}
3433

src/components/LevelSelect.tsx

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { MenuDivider, MenuItem } from '@blueprintjs/core'
2+
3+
import clsx from 'clsx'
4+
import Fuse from 'fuse.js'
5+
import { FC, Fragment, useMemo } from 'react'
6+
7+
import { useLevels } from '../apis/level'
8+
import { createCustomLevel, isHardMode } from '../models/level'
9+
import { Level } from '../models/operation'
10+
import { Suggest } from './Suggest'
11+
12+
interface LevelSelectProps {
13+
className?: string
14+
value: string
15+
onChange: (level: string) => void
16+
}
17+
18+
export const LevelSelect: FC<LevelSelectProps> = ({
19+
className,
20+
value,
21+
onChange,
22+
}) => {
23+
const { data } = useLevels()
24+
const levels = useMemo(
25+
() =>
26+
data
27+
// to simplify the list, we only show levels in normal mode
28+
.filter((level) => !isHardMode(level.stageId))
29+
.sort((a, b) => a.levelId.localeCompare(b.levelId)),
30+
[data],
31+
)
32+
33+
const fuse = useMemo(
34+
() =>
35+
new Fuse(levels, {
36+
keys: ['name', 'catTwo', 'catThree', 'stageId'],
37+
threshold: 0.3,
38+
}),
39+
[levels],
40+
)
41+
const fuseSimilar = useMemo(
42+
() =>
43+
new Fuse(levels, {
44+
keys: ['levelId'],
45+
threshold: 0,
46+
}),
47+
[levels],
48+
)
49+
50+
// value 可以由用户输入,所以可以是任何值,只有用 stageId 才能匹配到唯一的关卡
51+
const selectedLevel = useMemo(
52+
() => levels.find((el) => el.stageId === value) ?? null,
53+
[levels, value],
54+
)
55+
56+
return (
57+
<Suggest<Level>
58+
updateQueryOnSelect
59+
items={levels}
60+
itemListPredicate={(query) => {
61+
// 如果 query 和当前关卡完全匹配(也就是唯一对应),就显示同类关卡
62+
if (selectedLevel && selectedLevel.stageId === query) {
63+
const levelIdPrefix = selectedLevel.levelId
64+
.split('/')
65+
.slice(0, -1)
66+
.join('/')
67+
const similarLevels = fuseSimilar
68+
.search(levelIdPrefix)
69+
.map((el) => el.item)
70+
71+
if (similarLevels.length > 0) {
72+
const header = createCustomLevel('header')
73+
// catTwo 一般是活动名,有时候是空的
74+
header.catTwo = selectedLevel.catTwo || '相关关卡'
75+
return [header, ...similarLevels]
76+
}
77+
}
78+
79+
return query ? fuse.search(query).map((el) => el.item) : levels
80+
}}
81+
onReset={() => onChange('')}
82+
className={clsx(className, selectedLevel && '[&_input]:italic')}
83+
itemRenderer={(item, { handleClick, handleFocus, modifiers }) =>
84+
item.name === 'header' ? (
85+
<Fragment key="header">
86+
<div className="ml-2 text-zinc-500 text-xs">{item.catTwo}</div>
87+
<MenuDivider />
88+
</Fragment>
89+
) : (
90+
<MenuItem
91+
key={item.stageId}
92+
text={`${item.catThree} ${item.name}`}
93+
onClick={handleClick}
94+
onFocus={handleFocus}
95+
selected={modifiers.active}
96+
disabled={modifiers.disabled}
97+
/>
98+
)
99+
}
100+
selectedItem={selectedLevel}
101+
onItemSelect={(level) => onChange(level.stageId)}
102+
inputValueRenderer={(item) => item.stageId}
103+
noResults={<MenuItem disabled text="没有可选的关卡" />}
104+
inputProps={{
105+
placeholder: '关卡名、关卡类型、关卡编号',
106+
leftIcon: 'area-of-interest',
107+
large: true,
108+
size: 64,
109+
onBlur: (e) => {
110+
// 失焦时直接把 query 提交上去,用于处理关卡未匹配的情况
111+
if (value !== e.target.value) {
112+
onChange(e.target.value)
113+
}
114+
},
115+
}}
116+
/>
117+
)
118+
}

src/components/Operations.tsx

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { OperationSetList } from 'components/OperationSetList'
1717
import { neoLayoutAtom } from 'store/pref'
1818

1919
import { authAtom } from '../store/auth'
20+
import { LevelSelect } from './LevelSelect'
2021
import { OperatorFilter } from './OperatorFilter'
2122
import { withSuspensable } from './Suspensable'
2223

@@ -117,21 +118,15 @@ export const Operations: ComponentType = withSuspensable(() => {
117118
}
118119
onBlur={() => debouncedSetQueryParams.flush()}
119120
/>
120-
<InputGroup
121-
className="mt-2 [&>input]:!rounded-md"
122-
placeholder="关卡名、关卡类型、关卡编号"
123-
leftIcon="area-of-interest"
124-
size={64}
125-
large
126-
type="search"
127-
enterKeyHint="search"
128-
onChange={(e) =>
129-
debouncedSetQueryParams((old) => ({
121+
<LevelSelect
122+
className="mt-2"
123+
value={queryParams.levelKeyword ?? ''}
124+
onChange={(level) =>
125+
setQueryParams((old) => ({
130126
...old,
131-
levelKeyword: e.target.value.trim(),
127+
levelKeyword: level,
132128
}))
133129
}
134-
onBlur={() => debouncedSetQueryParams.flush()}
135130
/>
136131
<OperatorFilter
137132
className="mt-2"

src/components/Suggest.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,38 @@
11
import { Suggest2, Suggest2Props } from '@blueprintjs/select'
22

3-
import { useEffect, useMemo, useState } from 'react'
3+
import { noop } from 'lodash-es'
4+
import { useEffect, useMemo, useRef, useState } from 'react'
45
import { ControllerFieldState } from 'react-hook-form'
56

67
import { FieldResetButton } from './FieldResetButton'
78

89
interface SuggestProps<T> extends Suggest2Props<T> {
910
debounce?: number // defaults to 100(ms), set to 0 to disable
11+
updateQueryOnSelect?: boolean
1012
fieldState?: ControllerFieldState
1113
onReset?: () => void
1214
}
1315

1416
export const Suggest = <T,>({
1517
debounce = 100,
18+
updateQueryOnSelect,
1619
fieldState,
1720
onReset,
1821

1922
items,
2023
itemListPredicate,
24+
selectedItem,
25+
inputValueRenderer,
2126
inputProps,
2227
...suggest2Props
2328
}: SuggestProps<T>) => {
29+
// 禁用掉 focus 自动选中输入框文字的功能
30+
// https://github.com/palantir/blueprint/blob/b41f668461e63e2c20caf54a3248181fe01161c4/packages/select/src/components/suggest/suggest2.tsx#L229
31+
const ref = useRef<Suggest2<T>>(null)
32+
if (ref.current && ref.current['selectText'] !== noop) {
33+
ref.current['selectText'] = noop
34+
}
35+
2436
const [query, setQuery] = useState('')
2537
const [debouncedQuery, setDebouncedQuery] = useState('')
2638

@@ -46,11 +58,20 @@ export const Suggest = <T,>({
4658
}
4759
}, [fieldState?.isTouched])
4860

61+
useEffect(() => {
62+
if (updateQueryOnSelect && selectedItem) {
63+
setQuery(inputValueRenderer(selectedItem))
64+
}
65+
}, [updateQueryOnSelect, selectedItem, inputValueRenderer])
66+
4967
return (
5068
<Suggest2<T>
69+
ref={ref}
5170
items={filteredItems}
5271
query={query}
5372
onQueryChange={setQuery}
73+
selectedItem={selectedItem}
74+
inputValueRenderer={inputValueRenderer}
5475
inputProps={{
5576
onKeyDown: (event) => {
5677
// prevent form submission
@@ -60,7 +81,13 @@ export const Suggest = <T,>({
6081
},
6182
rightElement: (
6283
<FieldResetButton
63-
disabled={!fieldState?.isDirty}
84+
disabled={
85+
fieldState
86+
? !fieldState.isDirty
87+
: onReset
88+
? !(query || selectedItem !== null)
89+
: true
90+
}
6491
onReset={() => {
6592
setQuery('')
6693
setDebouncedQuery('')

0 commit comments

Comments
 (0)