Skip to content

Commit cba2764

Browse files
committed
feat: operation multiselect
1 parent 1754940 commit cba2764

File tree

5 files changed

+219
-70
lines changed

5 files changed

+219
-70
lines changed

src/components/OperationCard.tsx

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,17 @@ import { UserName } from './UserName'
1818
import { EDifficulty } from './entity/EDifficulty'
1919
import { EDifficultyLevel, NeoELevel } from './entity/ELevel'
2020

21-
export const NeoOperationCard = ({ operation }: { operation: Operation }) => {
21+
export const NeoOperationCard = ({
22+
operation,
23+
selected,
24+
selectable,
25+
onSelect,
26+
}: {
27+
operation: Operation
28+
selectable?: boolean
29+
selected?: boolean
30+
onSelect?: (operation: Operation, selected: boolean) => void
31+
}) => {
2232
const { data: levels } = useLevels()
2333

2434
return (
@@ -113,7 +123,13 @@ export const NeoOperationCard = ({ operation }: { operation: Operation }) => {
113123
</div>
114124
</ReLinkDiv>
115125

116-
<CardActions className="absolute top-4 right-4" operation={operation} />
126+
<CardActions
127+
className="absolute top-4 right-4"
128+
operation={operation}
129+
selectable={selectable}
130+
selected={selected}
131+
onSelect={onSelect}
132+
/>
117133
</Card>
118134
)
119135
}
@@ -247,11 +263,27 @@ const OperatorTags = ({ operation }: { operation: Operation }) => {
247263
const CardActions = ({
248264
className,
249265
operation,
266+
selected,
267+
selectable,
268+
onSelect,
250269
}: {
251270
className?: string
252271
operation: Operation
272+
selectable?: boolean
273+
selected?: boolean
274+
onSelect?: (operation: Operation, selected: boolean) => void
253275
}) => {
254-
return (
276+
return selectable ? (
277+
<Button
278+
small
279+
minimal={!selected}
280+
outlined={!selected}
281+
intent="primary"
282+
className="absolute top-4 right-4"
283+
icon={selected ? 'tick' : 'blank'}
284+
onClick={() => onSelect?.(operation, !selected)}
285+
/>
286+
) : (
255287
<div className={clsx('flex gap-1', className)}>
256288
<Tooltip2
257289
placement="bottom"
@@ -288,7 +320,11 @@ const CardActions = ({
288320
<div className="max-w-sm dark:text-slate-900">添加到作业集</div>
289321
}
290322
>
291-
<AddToOperationSetButton small icon="plus" operationId={operation.id} />
323+
<AddToOperationSetButton
324+
small
325+
icon="plus"
326+
operationIds={[operation.id]}
327+
/>
292328
</Tooltip2>
293329
</div>
294330
)

src/components/OperationList.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,19 @@ import { ComponentType, ReactNode, useEffect } from 'react'
66

77
import { neoLayoutAtom } from 'store/pref'
88

9+
import { Operation } from '../models/operation'
910
import { NeoOperationCard, OperationCard } from './OperationCard'
1011
import { withSuspensable } from './Suspensable'
1112

1213
interface OperationListProps extends UseOperationsParams {
14+
multiselect?: boolean
15+
selectedOperations?: Operation[]
16+
onSelect?: (operation: Operation, selected: boolean) => void
1317
onUpdate?: (params: { total: number }) => void
1418
}
1519

1620
export const OperationList: ComponentType<OperationListProps> = withSuspensable(
17-
({ onUpdate, ...params }) => {
21+
({ multiselect, selectedOperations, onSelect, onUpdate, ...params }) => {
1822
const neoLayout = useAtomValue(neoLayoutAtom)
1923

2024
const { operations, total, setSize, isValidating, isReachingEnd } =
@@ -38,7 +42,13 @@ export const OperationList: ComponentType<OperationListProps> = withSuspensable(
3842
}}
3943
>
4044
{operations.map((operation) => (
41-
<NeoOperationCard operation={operation} key={operation.id} />
45+
<NeoOperationCard
46+
operation={operation}
47+
key={operation.id}
48+
selectable={multiselect}
49+
selected={selectedOperations?.some((op) => op.id === operation.id)}
50+
onSelect={onSelect}
51+
/>
4252
))}
4353
</div>
4454
) : (

src/components/Operations.tsx

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
Button,
33
ButtonGroup,
4+
Callout,
45
Card,
56
Divider,
67
H6,
@@ -21,10 +22,12 @@ import { OperationList } from 'components/OperationList'
2122
import { OperationSetList } from 'components/OperationSetList'
2223
import { neoLayoutAtom } from 'store/pref'
2324

25+
import { Operation } from '../models/operation'
2426
import { LevelSelect } from './LevelSelect'
2527
import { OperatorFilter, useOperatorFilter } from './OperatorFilter'
2628
import { withSuspensable } from './Suspensable'
2729
import { UserFilter } from './UserFilter'
30+
import { AddToOperationSetButton } from './operation-set/AddToOperationSet'
2831

2932
export const Operations: ComponentType = withSuspensable(() => {
3033
const [queryParams, setQueryParams] = useState<
@@ -42,6 +45,8 @@ export const Operations: ComponentType = withSuspensable(() => {
4245
const [selectedUser, setSelectedUser] = useState<MaaUserInfo>()
4346
const [neoLayout, setNeoLayout] = useAtom(neoLayoutAtom)
4447
const [tab, setTab] = useState<'operation' | 'operationSet'>('operation')
48+
const [multiselect, setMultiselect] = useState(false)
49+
const [selectedOperations, setSelectedOperations] = useState<Operation[]>([])
4550

4651
return (
4752
<>
@@ -74,7 +79,15 @@ export const Operations: ComponentType = withSuspensable(() => {
7479
title="作业集"
7580
/>
7681
</Tabs>
77-
<ButtonGroup className="ml-auto">
82+
<Button
83+
minimal
84+
icon="multi-select"
85+
title="启动多选"
86+
className="ml-auto mr-2"
87+
active={multiselect}
88+
onClick={() => setMultiselect((v) => !v)}
89+
/>
90+
<ButtonGroup>
7891
<Button
7992
icon="grid-view"
8093
active={neoLayout}
@@ -178,6 +191,43 @@ export const Operations: ComponentType = withSuspensable(() => {
178191
</ButtonGroup>
179192
</div>
180193
</div>
194+
{multiselect && (
195+
<Callout className="mt-2 p-0 select-none">
196+
<details>
197+
<summary className="px-2 py-4 cursor-pointer hover:bg-zinc-500 hover:bg-opacity-5">
198+
已选择 {selectedOperations.length} 份作业
199+
</summary>
200+
<div className="p-2 flex flex-wrap gap-1">
201+
{selectedOperations.map((operation) => (
202+
<Button
203+
key={operation.id}
204+
small
205+
minimal
206+
rightIcon="cross"
207+
onClick={() =>
208+
setSelectedOperations((old) =>
209+
old.filter((op) => op.id !== operation.id),
210+
)
211+
}
212+
>
213+
{operation.parsedContent.doc.title}
214+
</Button>
215+
))}
216+
</div>
217+
</details>
218+
<AddToOperationSetButton
219+
minimal
220+
outlined
221+
intent="primary"
222+
icon="add-to-folder"
223+
className="absolute top-2 right-2"
224+
disabled={selectedOperations.length === 0}
225+
operationIds={selectedOperations.map((op) => op.id)}
226+
>
227+
添加到作业集
228+
</AddToOperationSetButton>
229+
</Callout>
230+
)}
181231
</>
182232
)}
183233

@@ -218,6 +268,17 @@ export const Operations: ComponentType = withSuspensable(() => {
218268
{tab === 'operation' && (
219269
<OperationList
220270
{...queryParams}
271+
multiselect={multiselect}
272+
selectedOperations={selectedOperations}
273+
onSelect={(operation, selected) =>
274+
setSelectedOperations((old) => {
275+
const newList = old.filter((op) => op.id !== operation.id)
276+
if (selected) {
277+
newList.push(operation)
278+
}
279+
return newList
280+
})
281+
}
221282
operator={operatorFilter.enabled ? operatorFilter : undefined}
222283
// 按热度排序时列表前几页的变化不会太频繁,可以不刷新第一页,节省点流量
223284
revalidateFirstPage={queryParams.orderBy !== 'hot'}

0 commit comments

Comments
 (0)