From cba27640ebac4a64911dbe9d198b5fc2ead32ad2 Mon Sep 17 00:00:00 2001 From: Guan <821143943@qq.com> Date: Sat, 12 Apr 2025 20:39:51 +0800 Subject: [PATCH 1/5] feat: operation multiselect --- src/components/OperationCard.tsx | 44 ++++- src/components/OperationList.tsx | 14 +- src/components/Operations.tsx | 63 ++++++- .../operation-set/AddToOperationSet.tsx | 159 +++++++++++------- src/styles/blueprint.less | 9 + 5 files changed, 219 insertions(+), 70 deletions(-) diff --git a/src/components/OperationCard.tsx b/src/components/OperationCard.tsx index 9cd29d7b..f8979ce2 100644 --- a/src/components/OperationCard.tsx +++ b/src/components/OperationCard.tsx @@ -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 ( @@ -113,7 +123,13 @@ export const NeoOperationCard = ({ operation }: { operation: Operation }) => { - + ) } @@ -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 ? ( + + ))} + + + op.id)} + > + 添加到作业集 + + + )} )} @@ -218,6 +268,17 @@ export const Operations: ComponentType = withSuspensable(() => { {tab === 'operation' && ( + setSelectedOperations((old) => { + const newList = old.filter((op) => op.id !== operation.id) + if (selected) { + newList.push(operation) + } + return newList + }) + } operator={operatorFilter.enabled ? operatorFilter : undefined} // 按热度排序时列表前几页的变化不会太频繁,可以不刷新第一页,节省点流量 revalidateFirstPage={queryParams.orderBy !== 'hot'} diff --git a/src/components/operation-set/AddToOperationSet.tsx b/src/components/operation-set/AddToOperationSet.tsx index 806b0169..b112ec84 100644 --- a/src/components/operation-set/AddToOperationSet.tsx +++ b/src/components/operation-set/AddToOperationSet.tsx @@ -6,6 +6,7 @@ import { Dialog, NonIdealState, Tag, + ToastProps, } from '@blueprintjs/core' import { @@ -15,7 +16,9 @@ import { } from 'apis/operation-set' import clsx from 'clsx' import { useAtomValue } from 'jotai' -import { useState } from 'react' +import { compact, isEqual } from 'lodash-es' +import { FC, memo, useState } from 'react' +import { useNavigate, useSearchParams } from 'react-router-dom' import { AppToaster } from 'components/Toaster' import { OperationSetEditorDialog } from 'components/operation-set/OperationSetEditor' @@ -25,44 +28,54 @@ import { useNetworkState } from 'utils/useNetworkState' import { authAtom } from '../../store/auth' interface AddToOperationSetButtonProps extends ButtonProps { - operationId: number + operationIds: number[] } -export function AddToOperationSetButton({ - operationId, - ...props -}: AddToOperationSetButtonProps) { - const [isOpen, setIsOpen] = useState(false) +export const AddToOperationSetButton: FC = memo( + ({ operationIds, ...props }) => { + const [isOpen, setIsOpen] = useState(false) - return ( - <> - +
+ {!!singleOperationId && ( + + setOnlyShowAdded((e.target as HTMLInputElement).checked) + } + /> + )} + + ))} +
+ +
+ + + + + op.id)} + > + 添加到作业集 + +
+ + )} + {items} {isReachingEnd && operations.length === 0 && ( diff --git a/src/components/Operations.tsx b/src/components/Operations.tsx index 61d515ce..05eb0a68 100644 --- a/src/components/Operations.tsx +++ b/src/components/Operations.tsx @@ -1,7 +1,6 @@ import { Button, ButtonGroup, - Callout, Card, Divider, H6, @@ -22,12 +21,10 @@ import { OperationList } from 'components/OperationList' import { OperationSetList } from 'components/OperationSetList' import { neoLayoutAtom } from 'store/pref' -import { Operation } from '../models/operation' import { LevelSelect } from './LevelSelect' import { OperatorFilter, useOperatorFilter } from './OperatorFilter' import { withSuspensable } from './Suspensable' import { UserFilter } from './UserFilter' -import { AddToOperationSetButton } from './operation-set/AddToOperationSet' export const Operations: ComponentType = withSuspensable(() => { const [queryParams, setQueryParams] = useState< @@ -46,7 +43,6 @@ export const Operations: ComponentType = withSuspensable(() => { const [neoLayout, setNeoLayout] = useAtom(neoLayoutAtom) const [tab, setTab] = useState<'operation' | 'operationSet'>('operation') const [multiselect, setMultiselect] = useState(false) - const [selectedOperations, setSelectedOperations] = useState([]) return ( <> @@ -191,43 +187,6 @@ export const Operations: ComponentType = withSuspensable(() => { - {multiselect && ( - -
- - 已选择 {selectedOperations.length} 份作业 - -
- {selectedOperations.map((operation) => ( - - ))} -
-
- op.id)} - > - 添加到作业集 - -
- )} )} @@ -269,16 +228,6 @@ export const Operations: ComponentType = withSuspensable(() => { - setSelectedOperations((old) => { - const newList = old.filter((op) => op.id !== operation.id) - if (selected) { - newList.push(operation) - } - return newList - }) - } operator={operatorFilter.enabled ? operatorFilter : undefined} // 按热度排序时列表前几页的变化不会太频繁,可以不刷新第一页,节省点流量 revalidateFirstPage={queryParams.orderBy !== 'hot'} From 67e3dc302869428eff4550f77a0c57569eea7ef6 Mon Sep 17 00:00:00 2001 From: Guan <821143943@qq.com> Date: Sun, 13 Apr 2025 01:22:38 +0800 Subject: [PATCH 3/5] feat: operation set auto sorting --- .../operation-set/OperationSetEditor.tsx | 121 +++++++++++++++--- 1 file changed, 104 insertions(+), 17 deletions(-) diff --git a/src/components/operation-set/OperationSetEditor.tsx b/src/components/operation-set/OperationSetEditor.tsx index c1128083..858125bf 100644 --- a/src/components/operation-set/OperationSetEditor.tsx +++ b/src/components/operation-set/OperationSetEditor.tsx @@ -6,9 +6,12 @@ import { DialogProps, Icon, InputGroup, + Menu, + MenuItem, NonIdealState, TextArea, } from '@blueprintjs/core' +import { Popover2 } from '@blueprintjs/popover2' import { DndContext, DragEndEvent, @@ -44,10 +47,13 @@ import { Controller, UseFormSetError, useForm } from 'react-hook-form' import { FormField } from 'components/FormField' import { AppToaster } from 'components/Toaster' import { Sortable } from 'components/dnd' -import { Operation } from 'models/operation' +import { Level, Operation } from 'models/operation' import { OperationSet } from 'models/operation-set' import { formatError } from 'utils/error' +import { useLevels } from '../../apis/level' +import { findLevelByStageName } from '../../models/level' + export function OperationSetEditorLauncher() { const [isOpen, setIsOpen] = useState(false) @@ -202,27 +208,22 @@ function OperationSetForm({ operationSet, onSubmit }: FormProps) { return (
-
+
{isEdit && ( -
+
{operationSet.copilotIds.length > 0 ? ( - <> -
- -
- + ) : ( @@ -236,7 +237,7 @@ function OperationSetForm({ operationSet, onSubmit }: FormProps) {
)} -
+
(