Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 40 additions & 4 deletions src/components/OperationCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,17 @@ import { UserName } from './UserName'
import { EDifficulty } from './entity/EDifficulty'
import { EDifficultyLevel, NeoELevel } from './entity/ELevel'

export const NeoOperationCard = ({ operation }: { operation: Operation }) => {
export const NeoOperationCard = ({
operation,
selected,
selectable,
onSelect,
}: {
operation: Operation
selectable?: boolean
selected?: boolean
onSelect?: (operation: Operation, selected: boolean) => void
}) => {
const { data: levels } = useLevels()

return (
Expand Down Expand Up @@ -113,7 +123,13 @@ export const NeoOperationCard = ({ operation }: { operation: Operation }) => {
</div>
</ReLinkDiv>

<CardActions className="absolute top-4 right-4" operation={operation} />
<CardActions
className="absolute top-4 right-4"
operation={operation}
selectable={selectable}
selected={selected}
onSelect={onSelect}
/>
</Card>
)
}
Expand Down Expand Up @@ -247,11 +263,27 @@ const OperatorTags = ({ operation }: { operation: Operation }) => {
const CardActions = ({
className,
operation,
selected,
selectable,
onSelect,
}: {
className?: string
operation: Operation
selectable?: boolean
selected?: boolean
onSelect?: (operation: Operation, selected: boolean) => void
}) => {
return (
return selectable ? (
<Button
small
minimal={!selected}
outlined={!selected}
intent="primary"
className="absolute top-4 right-4"
icon={selected ? 'tick' : 'blank'}
onClick={() => onSelect?.(operation, !selected)}
/>
) : (
<div className={clsx('flex gap-1', className)}>
<Tooltip2
placement="bottom"
Expand Down Expand Up @@ -288,7 +320,11 @@ const CardActions = ({
<div className="max-w-sm dark:text-slate-900">添加到作业集</div>
}
>
<AddToOperationSetButton small icon="plus" operationId={operation.id} />
<AddToOperationSetButton
small
icon="plus"
operationIds={[operation.id]}
/>
</Tooltip2>
</div>
)
Expand Down
91 changes: 87 additions & 4 deletions src/components/OperationList.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { Button, NonIdealState } from '@blueprintjs/core'
import { Button, Callout, NonIdealState } from '@blueprintjs/core'
import { Tooltip2 } from '@blueprintjs/popover2'

import { UseOperationsParams, useOperations } from 'apis/operation'
import { useAtomValue } from 'jotai'
import { ComponentType, ReactNode, useEffect } from 'react'
import { ComponentType, ReactNode, useEffect, useState } from 'react'

import { neoLayoutAtom } from 'store/pref'

import { Operation } from '../models/operation'
import { NeoOperationCard, OperationCard } from './OperationCard'
import { withSuspensable } from './Suspensable'
import { AddToOperationSetButton } from './operation-set/AddToOperationSet'

interface OperationListProps extends UseOperationsParams {
multiselect?: boolean
onUpdate?: (params: { total: number }) => void
}

export const OperationList: ComponentType<OperationListProps> = withSuspensable(
({ onUpdate, ...params }) => {
({ multiselect, onUpdate, ...params }) => {
const neoLayout = useAtomValue(neoLayoutAtom)

const { operations, total, setSize, isValidating, isReachingEnd } =
Expand All @@ -30,6 +34,25 @@ export const OperationList: ComponentType<OperationListProps> = withSuspensable(
onUpdate?.({ total })
}, [total, onUpdate])

const [selectedOperations, setSelectedOperations] = useState<Operation[]>(
[],
)
const updateSelection = (add: Operation[], remove: Operation[]) => {
setSelectedOperations((old) => {
return [
...old.filter((op) => !remove.some((o) => o.id === op.id)),
...add.filter((op) => !old.some((o) => o.id === op.id)),
]
})
}
const onSelect = (operation: Operation, selected: boolean) => {
if (selected) {
updateSelection([operation], [])
} else {
updateSelection([], [operation])
}
}

const items: ReactNode = neoLayout ? (
<div
className="grid gap-4"
Expand All @@ -38,7 +61,13 @@ export const OperationList: ComponentType<OperationListProps> = withSuspensable(
}}
>
{operations.map((operation) => (
<NeoOperationCard operation={operation} key={operation.id} />
<NeoOperationCard
operation={operation}
key={operation.id}
selectable={multiselect}
selected={selectedOperations?.some((op) => op.id === operation.id)}
onSelect={onSelect}
/>
))}
</div>
) : (
Expand All @@ -49,6 +78,60 @@ export const OperationList: ComponentType<OperationListProps> = withSuspensable(

return (
<>
{multiselect && (
<Callout className="mb-4 p-0 select-none">
<details>
<summary className="px-2 py-4 cursor-pointer hover:bg-zinc-500 hover:bg-opacity-5">
已选择 {selectedOperations.length} 份作业
</summary>
<div className="p-2 flex flex-wrap gap-1">
{selectedOperations.map((operation) => (
<Button
key={operation.id}
small
minimal
outlined
rightIcon="cross"
onClick={() => updateSelection([], [operation])}
>
{operation.parsedContent.doc.title}
</Button>
))}
</div>
</details>
<div className="absolute top-2 right-2 flex">
<Tooltip2 content="只能选择已加载的项目" placement="top">
<Button
minimal
icon="tick"
onClick={() => updateSelection(operations, [])}
>
全选
</Button>
</Tooltip2>
<Button
minimal
intent="danger"
icon="trash"
onClick={() => setSelectedOperations([])}
>
清空
</Button>
<AddToOperationSetButton
minimal
outlined
intent="primary"
icon="add-to-folder"
className="ml-2"
disabled={selectedOperations.length === 0}
operationIds={selectedOperations.map((op) => op.id)}
>
添加到作业集
</AddToOperationSetButton>
</div>
</Callout>
)}

{items}

{isReachingEnd && operations.length === 0 && (
Expand Down
12 changes: 11 additions & 1 deletion src/components/Operations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const Operations: ComponentType = withSuspensable(() => {
const [selectedUser, setSelectedUser] = useState<MaaUserInfo>()
const [neoLayout, setNeoLayout] = useAtom(neoLayoutAtom)
const [tab, setTab] = useState<'operation' | 'operationSet'>('operation')
const [multiselect, setMultiselect] = useState(false)

return (
<>
Expand Down Expand Up @@ -74,7 +75,15 @@ export const Operations: ComponentType = withSuspensable(() => {
title="作业集"
/>
</Tabs>
<ButtonGroup className="ml-auto">
<Button
minimal
icon="multi-select"
title="启动多选"
className="ml-auto mr-2"
active={multiselect}
onClick={() => setMultiselect((v) => !v)}
/>
<ButtonGroup>
<Button
icon="grid-view"
active={neoLayout}
Expand Down Expand Up @@ -218,6 +227,7 @@ export const Operations: ComponentType = withSuspensable(() => {
{tab === 'operation' && (
<OperationList
{...queryParams}
multiselect={multiselect}
operator={operatorFilter.enabled ? operatorFilter : undefined}
// 按热度排序时列表前几页的变化不会太频繁,可以不刷新第一页,节省点流量
revalidateFirstPage={queryParams.orderBy !== 'hot'}
Expand Down
Loading
Loading