From ea68945df11a14cd91809a63ba27f4a868dcf5b3 Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Wed, 1 May 2024 14:30:26 +0800 Subject: [PATCH] Feat(Menu): Add tool menu via drag and drop Drag and drop a tool card to add tool menu --- package-lock.json | 56 +++++ package.json | 3 + .../assets/css/components/common/sidebar.scss | 41 +++- .../css/components/dnd/drag-handle.scss | 5 + .../css/components/tools/repository-card.scss | 28 ++- .../css/components/tools/store-card.scss | 57 +++-- .../src/assets/css/pages/tools-framework.scss | 7 + .../src/assets/css/pages/tools/index.scss | 5 +- .../src/assets/css/pages/tools/store.scss | 2 +- .../src/assets/css/pages/tools/user.scss | 2 +- src/renderer/src/assets/svg/handle.svg | 1 + .../src/components/common/Sidebar/Item.tsx | 15 +- .../components/common/Sidebar/Separate.tsx | 2 +- .../src/components/dnd/DragHandle.tsx | 27 +++ src/renderer/src/components/dnd/Draggable.tsx | 47 ++++ .../src/components/dnd/DraggableOverlay.tsx | 22 ++ src/renderer/src/components/dnd/Droppable.tsx | 16 ++ .../src/components/dnd/HandleContext.ts | 13 + src/renderer/src/components/dnd/Sortable.tsx | 54 +++++ .../src/components/tools/RepositoryCard.tsx | 105 ++++---- .../src/components/tools/StoreCard.tsx | 172 ++++++++------ .../src/constants/common.constants.ts | 2 +- src/renderer/src/global.d.ts | 8 + src/renderer/src/pages/Tools/Store.tsx | 7 +- src/renderer/src/pages/Tools/User.tsx | 7 +- src/renderer/src/pages/Tools/View.tsx | 13 +- src/renderer/src/pages/Tools/index.tsx | 125 +++++----- src/renderer/src/pages/ToolsFramework.tsx | 224 +++++++++++++----- src/renderer/src/util/common.tsx | 14 ++ 29 files changed, 782 insertions(+), 298 deletions(-) create mode 100644 src/renderer/src/assets/css/components/dnd/drag-handle.scss create mode 100644 src/renderer/src/assets/svg/handle.svg create mode 100644 src/renderer/src/components/dnd/DragHandle.tsx create mode 100644 src/renderer/src/components/dnd/Draggable.tsx create mode 100644 src/renderer/src/components/dnd/DraggableOverlay.tsx create mode 100644 src/renderer/src/components/dnd/Droppable.tsx create mode 100644 src/renderer/src/components/dnd/HandleContext.ts create mode 100644 src/renderer/src/components/dnd/Sortable.tsx diff --git a/package-lock.json b/package-lock.json index 97ec8d1..2688f85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,9 @@ }, "devDependencies": { "@ant-design/icons": "^5.3.1", + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", + "@dnd-kit/utilities": "^3.2.2", "@electron-toolkit/eslint-config-prettier": "^2.0.0", "@electron-toolkit/eslint-config-ts": "^1.0.1", "@electron-toolkit/tsconfig": "^1.0.1", @@ -602,6 +605,59 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz", + "integrity": "sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==", + "dev": true, + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/@dnd-kit/core/-/core-6.1.0.tgz", + "integrity": "sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==", + "dev": true, + "dependencies": { + "@dnd-kit/accessibility": "^3.1.0", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/@dnd-kit/sortable/-/sortable-8.0.0.tgz", + "integrity": "sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==", + "dev": true, + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.1.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@electron-toolkit/eslint-config-prettier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@electron-toolkit/eslint-config-prettier/-/eslint-config-prettier-2.0.0.tgz", diff --git a/package.json b/package.json index ee5530b..1f6e452 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,9 @@ }, "devDependencies": { "@ant-design/icons": "^5.3.1", + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", + "@dnd-kit/utilities": "^3.2.2", "@electron-toolkit/eslint-config-prettier": "^2.0.0", "@electron-toolkit/eslint-config-ts": "^1.0.1", "@electron-toolkit/tsconfig": "^1.0.1", diff --git a/src/renderer/src/assets/css/components/common/sidebar.scss b/src/renderer/src/assets/css/components/common/sidebar.scss index 42190cd..6e3def4 100644 --- a/src/renderer/src/assets/css/components/common/sidebar.scss +++ b/src/renderer/src/assets/css/components/common/sidebar.scss @@ -55,13 +55,14 @@ .scroll { min-height: 0; flex: 1; + width: 100%; } ul { - > li { + > li, > div > li { + padding: 2px 14px; &.item { position: relative; - margin: 4px 14px; font-size: 1.4em; >.menu-bt { @@ -78,6 +79,10 @@ height: 40px; font-size: constants.$SIZE_ICON_SM; cursor: pointer; + + img{ + width: 100%; + } } a { @@ -86,6 +91,7 @@ height: 100%; width: 100%; transition: all 0.2s; + background-color: constants.$origin-color; .text { flex: 1; @@ -94,7 +100,12 @@ &.active { color: constants.$origin-color; - background-color: constants.$main-color !important; + background-color: constants.$main-color; + + img { + filter: drop-shadow(1000px 0 0 constants.$origin-color); + transform: translate(-1000px); + } } } } @@ -136,11 +147,11 @@ &.active { color: constants.$origin-color; - background-color: constants.$main-color !important; + background-color: constants.$main-color; } } - &:hover a { + &:hover a:not(.active) { background-color: constants.$background-color; } } @@ -149,7 +160,7 @@ &:hover { >.menu-bt { - a { + a:not(.active) { background-color: constants.$background-color; } } @@ -171,6 +182,22 @@ } } } + + .delete { + .menu-bt { + border: { + width: 1px; + color: constants.$error-secondary-color; + style: dashed; + }; + filter: drop-shadow(1000px 0 0 constants.$error-secondary-color); + transform: translate(-1000px); + + > a { + background-color: transparent !important; + } + } + } } } @@ -257,7 +284,7 @@ } .menu-bt { - .text { + .text, .extend { display: none; } } diff --git a/src/renderer/src/assets/css/components/dnd/drag-handle.scss b/src/renderer/src/assets/css/components/dnd/drag-handle.scss new file mode 100644 index 0000000..22b1088 --- /dev/null +++ b/src/renderer/src/assets/css/components/dnd/drag-handle.scss @@ -0,0 +1,5 @@ +[data-component=component-drag-handle] { + background-color: transparent; + color: unset; + cursor: grab; +} diff --git a/src/renderer/src/assets/css/components/tools/repository-card.scss b/src/renderer/src/assets/css/components/tools/repository-card.scss index 06d360e..b563c6f 100644 --- a/src/renderer/src/assets/css/components/tools/repository-card.scss +++ b/src/renderer/src/assets/css/components/tools/repository-card.scss @@ -1,6 +1,8 @@ @use '@/assets/css/constants' as constants; [data-component=component-repository-card] { + height: 100%; + .repository-card { width: 100%; height: 100%; @@ -12,23 +14,25 @@ flex: 0 0 auto; } - .version-select { - position: absolute; - top: 10px; - left: 10px; - width: 9em; - } + .header { + display: flex; + width: 100%; + align-items: center; + padding: 10px; - .upgrade-bt { - position: absolute; - top: 10px; - right: 10px; - font-size: 1.8em; + .version-select { + width: 9em; + margin-right: auto; + } + + >:not(.version-select) { + font-size: 1.6em; + } } .icon { display: flex; - padding-top: 50px; + padding-top: 10px; padding-bottom: 20px; color: constants.$production-color; font-size: constants.$SIZE_ICON_XL; diff --git a/src/renderer/src/assets/css/components/tools/store-card.scss b/src/renderer/src/assets/css/components/tools/store-card.scss index 15d0570..1652173 100644 --- a/src/renderer/src/assets/css/components/tools/store-card.scss +++ b/src/renderer/src/assets/css/components/tools/store-card.scss @@ -1,6 +1,7 @@ @use '@/assets/css/constants' as constants; [data-component=component-store-card] { + height: 100%; cursor: pointer; .store-card { @@ -14,10 +15,31 @@ flex: 0 0 auto; } + .header { + display: flex; + width: 100%; + padding: 10px; + justify-content: space-between; + + .version { + width: 0; + } + + .operation { + display: flex; + font-size: 1.6em; + gap: 4px; + opacity: 0; + + > *:hover { + color: constants.$font-secondary-color; + } + } + } .icon { display: flex; - padding-top: 40px; + padding-top: 10px; padding-bottom: 20px; color: constants.$production-color; font-size: constants.$SIZE_ICON_XL; @@ -28,12 +50,6 @@ } } - .version { - position: absolute; - left: 10px; - top: 10px; - } - .info { padding-top: 20px; @@ -43,8 +59,16 @@ } .tool-desc { - margin-top: 10px; + margin: { + top: 10px; + left: auto; + right: auto; + }; color: constants.$font-secondary-color; + overflow: hidden; + text-overflow: ellipsis; + max-height: 40px; + width: 80%; } } @@ -68,17 +92,16 @@ align-items: center; } } + } - .operation { - display: flex; - position: absolute; - top: 10px; - right: 12px; - font-size: 1.6em; - gap: 4px; + :hover { + .header { + .version { + opacity: 0; + } - > *:hover { - color: constants.$font-secondary-color; + .operation { + opacity: 1; } } } diff --git a/src/renderer/src/assets/css/pages/tools-framework.scss b/src/renderer/src/assets/css/pages/tools-framework.scss index 79d055f..137c254 100644 --- a/src/renderer/src/assets/css/pages/tools-framework.scss +++ b/src/renderer/src/assets/css/pages/tools-framework.scss @@ -4,6 +4,13 @@ [data-component=tools-framework] { .left-panel { background-color: constants.$origin-color; + + .menu-droppable { + display: flex; + min-height: 0; + flex: 1; + width: 100%; + } } .right-panel { diff --git a/src/renderer/src/assets/css/pages/tools/index.scss b/src/renderer/src/assets/css/pages/tools/index.scss index db8f1a9..95a2196 100644 --- a/src/renderer/src/assets/css/pages/tools/index.scss +++ b/src/renderer/src/assets/css/pages/tools/index.scss @@ -11,7 +11,7 @@ flex-wrap: wrap; justify-content: flex-start; - > .card-box { + > .card-box, > div { width: 180px; height: 290px; flex: 0 0 auto; @@ -77,6 +77,7 @@ flex-direction: row; align-items: center; gap: 20px; + margin-top: 20px; :first-child, :last-child { height: 0; @@ -99,7 +100,7 @@ flex-wrap: wrap; justify-content: flex-start; - > .card-box { + > .card-box, > div { width: 180px; height: 290px; flex: 0 0 auto; diff --git a/src/renderer/src/assets/css/pages/tools/store.scss b/src/renderer/src/assets/css/pages/tools/store.scss index b87a3f5..3bead2e 100644 --- a/src/renderer/src/assets/css/pages/tools/store.scss +++ b/src/renderer/src/assets/css/pages/tools/store.scss @@ -26,7 +26,7 @@ flex-wrap: wrap; justify-content: center; - > .card-box { + > div { width: 180px; height: 290px; flex: 0 0 auto; diff --git a/src/renderer/src/assets/css/pages/tools/user.scss b/src/renderer/src/assets/css/pages/tools/user.scss index 97eb6c0..54d0e74 100644 --- a/src/renderer/src/assets/css/pages/tools/user.scss +++ b/src/renderer/src/assets/css/pages/tools/user.scss @@ -68,7 +68,7 @@ flex-wrap: wrap; justify-content: center; - > .card-box { + > div { width: 180px; height: 290px; flex: 0 0 auto; diff --git a/src/renderer/src/assets/svg/handle.svg b/src/renderer/src/assets/svg/handle.svg new file mode 100644 index 0000000..5d9156f --- /dev/null +++ b/src/renderer/src/assets/svg/handle.svg @@ -0,0 +1 @@ + diff --git a/src/renderer/src/components/common/Sidebar/Item.tsx b/src/renderer/src/components/common/Sidebar/Item.tsx index 1031b15..7f6885d 100644 --- a/src/renderer/src/components/common/Sidebar/Item.tsx +++ b/src/renderer/src/components/common/Sidebar/Item.tsx @@ -3,10 +3,11 @@ import Icon from '@ant-design/icons' import Submenu from '@/components/common/Sidebar/Submenu' type ItemProps = { - icon?: IconComponent + icon?: IconComponent | string text?: string path: string children?: ReactNode + extend?: ReactNode end?: boolean } @@ -42,9 +43,19 @@ const Item = (props: ItemProps) => { } >
- {props.icon && } + {props.icon && + (typeof props.icon === 'string' ? ( + {'icon'} + ) : ( + + ))}
{props.text} +
{props.extend}
{props.children && ( diff --git a/src/renderer/src/components/common/Sidebar/Separate.tsx b/src/renderer/src/components/common/Sidebar/Separate.tsx index 4c42aff..b1ffa0f 100644 --- a/src/renderer/src/components/common/Sidebar/Separate.tsx +++ b/src/renderer/src/components/common/Sidebar/Separate.tsx @@ -4,7 +4,7 @@ const Separate = ({ className, ...props }: DetailedHTMLProps, HTMLDivElement>) => { - return
+ return
} export default Separate diff --git a/src/renderer/src/components/dnd/DragHandle.tsx b/src/renderer/src/components/dnd/DragHandle.tsx new file mode 100644 index 0000000..eb81812 --- /dev/null +++ b/src/renderer/src/components/dnd/DragHandle.tsx @@ -0,0 +1,27 @@ +import { HandleContextInst } from '@/components/dnd/HandleContext' +import Icon from '@ant-design/icons' +import '@/assets/css/components/dnd/drag-handle.scss' + +interface DragHandleProps { + padding?: string | number +} + +const DragHandle = ({ padding }: DragHandleProps) => { + // eslint-disable-next-line @typescript-eslint/unbound-method + const { attributes, listeners, ref } = useContext(HandleContextInst) + + return ( + + ) +} + +export default DragHandle diff --git a/src/renderer/src/components/dnd/Draggable.tsx b/src/renderer/src/components/dnd/Draggable.tsx new file mode 100644 index 0000000..756570d --- /dev/null +++ b/src/renderer/src/components/dnd/Draggable.tsx @@ -0,0 +1,47 @@ +import { CSSProperties, PropsWithChildren } from 'react' +import { useDraggable } from '@dnd-kit/core' +import { HandleContext, HandleContextInst } from '@/components/dnd/HandleContext' + +interface DraggableProps extends PropsWithChildren { + id: string + data: ToolMenuItem +} + +const Draggable = ({ id, data, children }: DraggableProps) => { + const { + attributes, + isDragging, + listeners, + setNodeRef: draggableRef, + setActivatorNodeRef, + transform + } = useDraggable({ + id, + data + }) + const context = useMemo( + () => ({ + attributes, + listeners, + ref: setActivatorNodeRef + }), + [attributes, listeners, setActivatorNodeRef] + ) + const style: CSSProperties | undefined = transform + ? { + opacity: isDragging ? 0 : undefined, + transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`, + zIndex: 10000 + } + : undefined + + return ( + +
+ {children} +
+
+ ) +} + +export default Draggable diff --git a/src/renderer/src/components/dnd/DraggableOverlay.tsx b/src/renderer/src/components/dnd/DraggableOverlay.tsx new file mode 100644 index 0000000..0e4f3fd --- /dev/null +++ b/src/renderer/src/components/dnd/DraggableOverlay.tsx @@ -0,0 +1,22 @@ +import { PropsWithChildren } from 'react' +import { defaultDropAnimationSideEffects, DragOverlay, DropAnimation } from '@dnd-kit/core' + +interface DraggableOverlayProps extends PropsWithChildren { + isDelete?: boolean +} + +const dropAnimationConfig: DropAnimation = { + sideEffects: defaultDropAnimationSideEffects({ + styles: { + active: { + opacity: '0.4' + } + } + }) +} + +const DraggableOverlay = ({ children }: DraggableOverlayProps) => { + return {children} +} + +export default DraggableOverlay diff --git a/src/renderer/src/components/dnd/Droppable.tsx b/src/renderer/src/components/dnd/Droppable.tsx new file mode 100644 index 0000000..08229b0 --- /dev/null +++ b/src/renderer/src/components/dnd/Droppable.tsx @@ -0,0 +1,16 @@ +import { DetailedHTMLProps, HTMLAttributes } from 'react' +import { useDroppable } from '@dnd-kit/core' + +interface DroppableProps extends DetailedHTMLProps, HTMLDivElement> { + id: string +} + +const Droppable = ({ id, ...props }: DroppableProps) => { + const { setNodeRef: droppableRef } = useDroppable({ + id + }) + + return
+} + +export default Droppable diff --git a/src/renderer/src/components/dnd/HandleContext.ts b/src/renderer/src/components/dnd/HandleContext.ts new file mode 100644 index 0000000..88420b2 --- /dev/null +++ b/src/renderer/src/components/dnd/HandleContext.ts @@ -0,0 +1,13 @@ +import { DraggableSyntheticListeners } from '@dnd-kit/core' + +export interface HandleContext { + attributes: Record + listeners: DraggableSyntheticListeners + ref(node: HTMLElement | null): void +} + +export const HandleContextInst = createContext({ + attributes: {}, + listeners: undefined, + ref() {} +}) diff --git a/src/renderer/src/components/dnd/Sortable.tsx b/src/renderer/src/components/dnd/Sortable.tsx new file mode 100644 index 0000000..81cd0b7 --- /dev/null +++ b/src/renderer/src/components/dnd/Sortable.tsx @@ -0,0 +1,54 @@ +import { CSSProperties, PropsWithChildren } from 'react' +import { useSortable } from '@dnd-kit/sortable' +import { HandleContext, HandleContextInst } from '@/components/dnd/HandleContext' + +interface SortableProps extends PropsWithChildren { + id: string + data: ToolMenuItem + isDelete?: boolean +} + +const Sortable = ({ id, data, isDelete, children }: SortableProps) => { + const { + attributes, + isDragging, + listeners, + setNodeRef: draggableRef, + setActivatorNodeRef, + transform, + transition + } = useSortable({ + id, + data + }) + const context = useMemo( + () => ({ + attributes, + listeners, + ref: setActivatorNodeRef + }), + [attributes, listeners, setActivatorNodeRef] + ) + const style: CSSProperties | undefined = transform + ? { + opacity: isDragging ? 0.4 : undefined, + transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`, + zIndex: 10000, + transition + } + : undefined + + return ( + +
+ {children} +
+
+ ) +} + +export default Sortable diff --git a/src/renderer/src/components/tools/RepositoryCard.tsx b/src/renderer/src/components/tools/RepositoryCard.tsx index eebe6dd..bbbe8d6 100644 --- a/src/renderer/src/components/tools/RepositoryCard.tsx +++ b/src/renderer/src/components/tools/RepositoryCard.tsx @@ -1,14 +1,17 @@ -import { DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react' +import { DetailedHTMLProps, HTMLAttributes } from 'react' import VanillaTilt, { TiltOptions } from 'vanilla-tilt' import '@/assets/css/components/tools/repository-card.scss' import Card from '@/components/common/Card' import FlexBox from '@/components/common/FlexBox' +import Draggable from '@/components/dnd/Draggable' +import DragHandle from '@/components/dnd/DragHandle.tsx' interface RepositoryCardProps extends DetailedHTMLProps, HTMLDivElement> { - icon: ReactNode + icon: string toolName: string toolId: string + ver: string options?: TiltOptions onOpen?: () => void onEdit?: () => void @@ -25,6 +28,7 @@ const RepositoryCard = ({ icon, toolName, toolId, + ver, options = { reverse: true, max: 8, @@ -48,51 +52,58 @@ const RepositoryCard = ({ }, [options]) return ( - - -
{icon}
-
- {toolName &&
{toolName}
} - {toolId &&
{`ID: ${toolId}`}
} -
-
- {onOpen && ( - - 打开 - - )} - {onEdit && onPublish && ( -
- - 编辑 - 发布 - -
- )} - {onSource && ( - - 源码 - - )} - {onCancelReview && ( - - 取消审核 - - )} - {onDelete && ( - - 删除 - - )} -
- {children} -
-
+ + + +
+ {children} + +
+
+ {'Icon'} +
+
+
{toolName}
+
{`ID: ${toolId}`}
+
+
+ {onOpen && ( + + 打开 + + )} + {onEdit && onPublish && ( +
+ + 编辑 + 发布 + +
+ )} + {onSource && ( + + 源码 + + )} + {onCancelReview && ( + + 取消审核 + + )} + {onDelete && ( + + 删除 + + )} +
+
+
+
) } diff --git a/src/renderer/src/components/tools/StoreCard.tsx b/src/renderer/src/components/tools/StoreCard.tsx index 43f32da..0a77fb8 100644 --- a/src/renderer/src/components/tools/StoreCard.tsx +++ b/src/renderer/src/components/tools/StoreCard.tsx @@ -1,4 +1,4 @@ -import { DetailedHTMLProps, HTMLAttributes, MouseEvent, ReactNode } from 'react' +import { DetailedHTMLProps, HTMLAttributes, MouseEvent } from 'react' import VanillaTilt, { TiltOptions } from 'vanilla-tilt' import protocolCheck from 'custom-protocol-check' import Icon from '@ant-design/icons' @@ -10,9 +10,11 @@ import { r_tool_add_favorite, r_tool_remove_favorite } from '@/services/tool' import Card from '@/components/common/Card' import FlexBox from '@/components/common/FlexBox' import { getUserId } from '@/util/auth.tsx' +import DragHandle from '@/components/dnd/DragHandle.tsx' +import Draggable from '@/components/dnd/Draggable.tsx' interface StoreCardProps extends DetailedHTMLProps, HTMLDivElement> { - icon: ReactNode + icon: string toolName: string toolId: string toolDesc: string @@ -40,8 +42,8 @@ const StoreCard = ({ ['max-glare']: 0.3, scale: 1.03 }, - author, - showAuthor = true, + author, + showAuthor = true, ver, platform, supportPlatform, @@ -176,81 +178,101 @@ const StoreCard = ({ return ( <> - - -
{icon}
-
- - {platform.slice(0, 1)}-{ver} - -
-
-
{toolName}
-
{`ID: ${toolId}`}
- {toolDesc &&
{`简介:${toolDesc}`}
} -
- {showAuthor && ( -
-
- - } - style={{ background: COLOR_BACKGROUND }} - /> + + +
+
+ + {platform.slice(0, 1)}-{ver} + +
+
+ {platform !== 'ANDROID' && supportPlatform.includes('ANDROID') && ( + + + + )} + {platform === 'DESKTOP' && supportPlatform.includes('WEB') && ( + + + + )} + {platform === 'WEB' && supportPlatform.includes('DESKTOP') && ( + + + + )} + + + + {author.id !== userId && ( + + + + )} +
- -
{author.userInfo.nickname}
-
- )} -
- {platform !== 'ANDROID' && supportPlatform.includes('ANDROID') && ( - - - +
+ {'Icon'} +
+
+
{toolName}
+
{`ID: ${toolId}`}
+ {toolDesc &&
{`简介:${toolDesc}`}
} +
+ {showAuthor && ( +
+
+ + } + style={{ background: COLOR_BACKGROUND }} + /> +
+ +
{author.userInfo.nickname}
+
+
)} - {platform === 'DESKTOP' && supportPlatform.includes('WEB') && ( - - - - )} - {platform === 'WEB' && supportPlatform.includes('DESKTOP') && ( - - - - )} - - - - {author.id !== userId && ( - - - - )} -
-
-
+ + + {contextHolder} ) diff --git a/src/renderer/src/constants/common.constants.ts b/src/renderer/src/constants/common.constants.ts index ead8e99..99e5df7 100644 --- a/src/renderer/src/constants/common.constants.ts +++ b/src/renderer/src/constants/common.constants.ts @@ -1,7 +1,7 @@ export const PRODUCTION_NAME = 'Oxygen Toolbox' export const STORAGE_TOKEN_KEY = 'JWT_TOKEN' export const STORAGE_USER_INFO_KEY = 'USER_INFO' -export const STORAGE_FAVORITE_KEY = 'FAVORITE' +export const STORAGE_TOOL_MENU_ITEM_KEY = 'TOOL_MENU_ITEM' export const COLOR_ORIGIN = 'white' export const COLOR_PRODUCTION = '#4E47BB' export const COLOR_MAIN = COLOR_PRODUCTION diff --git a/src/renderer/src/global.d.ts b/src/renderer/src/global.d.ts index 233d760..fbed311 100644 --- a/src/renderer/src/global.d.ts +++ b/src/renderer/src/global.d.ts @@ -661,3 +661,11 @@ interface ToolFavoriteAddRemoveParam { toolId: string platform: Platform } + +interface ToolMenuItem { + icon: string + toolName: string + toolId: string + authorUsername: string + ver: string +} diff --git a/src/renderer/src/pages/Tools/Store.tsx b/src/renderer/src/pages/Tools/Store.tsx index f187751..eca029d 100644 --- a/src/renderer/src/pages/Tools/Store.tsx +++ b/src/renderer/src/pages/Tools/Store.tsx @@ -134,12 +134,7 @@ const Store = () => { return ( - } + icon={firstTool!.icon} toolName={firstTool!.name} toolId={firstTool!.toolId} toolDesc={firstTool!.description} diff --git a/src/renderer/src/pages/Tools/User.tsx b/src/renderer/src/pages/Tools/User.tsx index 2124196..4fb4cd1 100644 --- a/src/renderer/src/pages/Tools/User.tsx +++ b/src/renderer/src/pages/Tools/User.tsx @@ -197,12 +197,7 @@ const User = () => { return ( - } + icon={firstTool!.icon} toolName={firstTool!.name} toolId={firstTool!.toolId} toolDesc={firstTool!.description} diff --git a/src/renderer/src/pages/Tools/View.tsx b/src/renderer/src/pages/Tools/View.tsx index d61e981..ab10c0b 100644 --- a/src/renderer/src/pages/Tools/View.tsx +++ b/src/renderer/src/pages/Tools/View.tsx @@ -32,7 +32,10 @@ const View = () => { .compile(files, importMap, toolVo.entryPoint) .then((result) => { const output = result.outputFiles[0].text - setCompiledCode(`${output}\n${baseDist}`) + setCompiledCode('') + setTimeout(() => { + setCompiledCode(`${output}\n${baseDist}`) + }) }) .catch((reason) => { void message.error(`编译失败:${reason}`) @@ -72,9 +75,7 @@ const View = () => { break case DATABASE_NO_RECORD_FOUND: void message.error('未找到指定工具') - setTimeout(() => { - navigateToRepository(navigate) - }, 3000) + navigateToRepository(navigate) break default: void message.error('获取工具信息失败,请稍后重试') @@ -103,11 +104,11 @@ const View = () => { return } if (username === '!' && !ver) { - navigateToView(navigate, '!', toolId!, platform as Platform) + navigateToView(navigate, '!', toolId!, platform as Platform, 'latest') return } getTool() - }, []) + }, [username, toolId, ver, searchParams]) return ( diff --git a/src/renderer/src/pages/Tools/index.tsx b/src/renderer/src/pages/Tools/index.tsx index 9269bbb..1a3d848 100644 --- a/src/renderer/src/pages/Tools/index.tsx +++ b/src/renderer/src/pages/Tools/index.tsx @@ -166,9 +166,10 @@ const ToolCard = ({ tools, onDelete, onUpgrade, onSubmit, onCancel }: ToolCardPr return ( } + icon={selectedTool.icon} toolName={selectedTool.name} toolId={selectedTool.toolId} + ver={selectedTool.ver} onOpen={handleOnOpenTool} onEdit={handleOnEditTool()} onSource={handleOnSourceTool()} @@ -563,65 +564,71 @@ const Tools = () => { ))} {hasNextPage && } - -
-
收藏
-
- - - {starToolData - ?.reduce((previousValue: ToolVo[], currentValue) => { - if ( - !previousValue.some( - (value) => - value.author.id === currentValue.author.id && - value.toolId === currentValue.toolId - ) - ) { - previousValue.push(currentValue) - } - return previousValue - }, []) - .map((item) => { - const tools = starToolData.filter( - (value) => - value.author.id === item.author.id && - value.toolId === item.toolId - ) - const webTool = tools.find((value) => value.platform === 'WEB') - const desktopTool = tools.find( - (value) => value.platform === 'DESKTOP' - ) - const androidTool = tools.find( - (value) => value.platform === 'ANDROID' - ) - const firstTool = - (checkDesktop() - ? desktopTool || webTool - : webTool || desktopTool) || androidTool - - return ( - + {starToolData.length ? ( + <> + +
+
收藏
+
+ + + {starToolData + ?.reduce((previousValue: ToolVo[], currentValue) => { + if ( + !previousValue.some( + (value) => + value.author.id === + currentValue.author.id && + value.toolId === currentValue.toolId + ) + ) { + previousValue.push(currentValue) } - toolName={firstTool!.name} - toolId={firstTool!.toolId} - toolDesc={firstTool!.description} - author={firstTool!.author} - ver={firstTool!.ver} - platform={firstTool!.platform} - supportPlatform={tools.map((value) => value.platform)} - favorite={firstTool!.favorite} - /> - ) - })} - {hasNextStarPage && } - + return previousValue + }, []) + .map((item) => { + const tools = starToolData.filter( + (value) => + value.author.id === item.author.id && + value.toolId === item.toolId + ) + const webTool = tools.find( + (value) => value.platform === 'WEB' + ) + const desktopTool = tools.find( + (value) => value.platform === 'DESKTOP' + ) + const androidTool = tools.find( + (value) => value.platform === 'ANDROID' + ) + const firstTool = + (checkDesktop() + ? desktopTool || webTool + : webTool || desktopTool) || androidTool + + return ( + value.platform + )} + favorite={firstTool!.favorite} + /> + ) + })} + {hasNextStarPage && ( + + )} + + + ) : undefined} diff --git a/src/renderer/src/pages/ToolsFramework.tsx b/src/renderer/src/pages/ToolsFramework.tsx index 13bd59c..4b284cb 100644 --- a/src/renderer/src/pages/ToolsFramework.tsx +++ b/src/renderer/src/pages/ToolsFramework.tsx @@ -1,70 +1,184 @@ +import { DndContext, DragOverEvent, DragStartEvent } from '@dnd-kit/core' +import Droppable from '@/components/dnd/Droppable' +import type { DragEndEvent } from '@dnd-kit/core/dist/types' import '@/assets/css/pages/tools-framework.scss' import { tools } from '@/router/tools' import FitFullscreen from '@/components/common/FitFullscreen' import Sidebar from '@/components/common/Sidebar' import FullscreenLoadingMask from '@/components/common/FullscreenLoadingMask' +import { arrayMove, SortableContext } from '@dnd-kit/sortable' +import { getViewPath } from '@/util/navigation.tsx' +import Sortable from '@/components/dnd/Sortable.tsx' +import DragHandle from '@/components/dnd/DragHandle.tsx' +import DraggableOverlay from '@/components/dnd/DraggableOverlay.tsx' +import { getToolMenuItem, saveToolMenuItem } from '@/util/common.tsx' const ToolsFramework = () => { + const [isDelete, setIsDelete] = useState(false) + const [toolMenuItem, setToolMenuItem] = useState(getToolMenuItem) + const [activeItem, setActiveItem] = useState(null) + + const handleOnDragStart = ({ active }: DragStartEvent) => { + setActiveItem(active.data.current as ToolMenuItem) + } + + const handleOnDragOver = ({ over }: DragOverEvent) => { + setIsDelete(over === null) + } + + const handleOnDragEnd = ({ active, over }: DragEndEvent) => { + if (over && active.id !== over?.id) { + const activeIndex = toolMenuItem.findIndex( + ({ authorUsername, toolId, ver }) => + `${authorUsername}:${toolId}:${ver}` === active.id + ) + const overIndex = toolMenuItem.findIndex( + ({ authorUsername, toolId, ver }) => + `${authorUsername}:${toolId}:${ver}` === over.id + ) + setToolMenuItem(arrayMove(toolMenuItem, activeIndex, overIndex)) + } + + if (!over) { + setToolMenuItem( + toolMenuItem.filter( + ({ authorUsername, toolId, ver }) => + `${authorUsername}:${toolId}:${ver}` !== active?.id + ) + ) + } + + if (!active.data.current?.sortable && over) { + const newItem = active.data.current as ToolMenuItem + if ( + toolMenuItem.findIndex( + ({ authorUsername, toolId, ver }) => + authorUsername === newItem.authorUsername && + toolId === newItem.toolId && + ver === newItem.ver + ) === -1 + ) { + setToolMenuItem([...toolMenuItem, newItem]) + } + } + + console.log('active', active) + console.log('over', over) + + setActiveItem(null) + } + + const handleOnDragCancel = () => { + setActiveItem(null) + } + + useEffect(() => { + saveToolMenuItem(toolMenuItem) + }, [toolMenuItem]) + return ( <> -
- - - - - - - + +
+ - {tools.map((tool) => { - return tool.menu && - tool.id !== 'tools-store' && - tool.id !== 'tools-repository' ? ( - - {tool.children && - tool.children.map((subTool) => { - return ( - - ) - })} - - ) : undefined - })} + + - - -
-
- - - - } - > - - -
+ + + + + + `${authorUsername}:${toolId}:${ver}` + )} + > + {toolMenuItem.map( + ({ + icon, + toolName, + toolId, + authorUsername, + ver + }) => ( + + } + /> + + ) + )} + + + {activeItem && ( + } + /> + )} + + + + +
+
+
+ + + + } + > + + +
+
) diff --git a/src/renderer/src/util/common.tsx b/src/renderer/src/util/common.tsx index f32740e..f6c6e9c 100644 --- a/src/renderer/src/util/common.tsx +++ b/src/renderer/src/util/common.tsx @@ -1,6 +1,8 @@ import { createRoot } from 'react-dom/client' import FullscreenLoadingMask from '@/components/common/FullscreenLoadingMask' import { floor } from 'lodash' +import { getLocalStorage, setLocalStorage } from '@/util/browser.tsx' +import { STORAGE_TOOL_MENU_ITEM_KEY } from '@/constants/common.constants.ts' export const randomInt = (start: number, end: number) => { if (start > end) { @@ -133,3 +135,15 @@ const formatByte = (size: number, unit: ByteUnit): string => { } export const checkDesktop = () => import.meta.env.VITE_PLATFORM === 'DESKTOP' + +export const saveToolMenuItem = (toolMenuItem: ToolMenuItem[]) => { + setLocalStorage(STORAGE_TOOL_MENU_ITEM_KEY, JSON.stringify(toolMenuItem)) +} + +export const getToolMenuItem = (): ToolMenuItem[] => { + const s = getLocalStorage(STORAGE_TOOL_MENU_ITEM_KEY) + if (!s) { + return [] + } + return JSON.parse(s) as ToolMenuItem[] +}