Feat(Menu): Add tool menu via drag and drop

Drag and drop a tool card to add tool menu
This commit is contained in:
2024-04-30 13:42:36 +08:00
parent 843f47346a
commit 7b61a5fdb3
30 changed files with 785 additions and 298 deletions

55
package-lock.json generated
View File

@@ -9,6 +9,9 @@
"version": "1.0.0-SNAPSHOT", "version": "1.0.0-SNAPSHOT",
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.2.6", "@ant-design/icons": "^5.2.6",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@marsidev/react-turnstile": "^0.5.3", "@marsidev/react-turnstile": "^0.5.3",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@typescript/ata": "^0.9.4", "@typescript/ata": "^0.9.4",
@@ -584,6 +587,55 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz",
"integrity": "sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.1.0.tgz",
"integrity": "sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==",
"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.npmjs.org/@dnd-kit/sortable/-/sortable-8.0.0.tgz",
"integrity": "sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==",
"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.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/hash": { "node_modules/@emotion/hash": {
"version": "0.8.0", "version": "0.8.0",
"resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz", "resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz",
@@ -7428,8 +7480,7 @@
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.6.2", "version": "2.6.2",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.6.2.tgz", "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
"dev": true
}, },
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",

View File

@@ -16,6 +16,9 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.2.6", "@ant-design/icons": "^5.2.6",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@marsidev/react-turnstile": "^0.5.3", "@marsidev/react-turnstile": "^0.5.3",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@typescript/ata": "^0.9.4", "@typescript/ata": "^0.9.4",

View File

@@ -55,13 +55,14 @@
.scroll { .scroll {
min-height: 0; min-height: 0;
flex: 1; flex: 1;
width: 100%;
} }
ul { ul {
> li { > li, > div > li {
padding: 2px 14px;
&.item { &.item {
position: relative; position: relative;
margin: 4px 14px;
font-size: 1.4em; font-size: 1.4em;
>.menu-bt { >.menu-bt {
@@ -78,6 +79,10 @@
height: 40px; height: 40px;
font-size: constants.$SIZE_ICON_SM; font-size: constants.$SIZE_ICON_SM;
cursor: pointer; cursor: pointer;
img{
width: 100%;
}
} }
a { a {
@@ -86,6 +91,7 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
transition: all 0.2s; transition: all 0.2s;
background-color: constants.$origin-color;
.text { .text {
flex: 1; flex: 1;
@@ -94,7 +100,12 @@
&.active { &.active {
color: constants.$origin-color; 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 { &.active {
color: constants.$origin-color; 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; background-color: constants.$background-color;
} }
} }
@@ -149,7 +160,7 @@
&:hover { &:hover {
>.menu-bt { >.menu-bt {
a { a:not(.active) {
background-color: constants.$background-color; 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 { .menu-bt {
.text { .text, .extend {
display: none; display: none;
} }
} }

View File

@@ -0,0 +1,5 @@
[data-component=component-drag-handle] {
background-color: transparent;
color: unset;
cursor: grab;
}

View File

@@ -1,6 +1,8 @@
@use '@/assets/css/constants' as constants; @use '@/assets/css/constants' as constants;
[data-component=component-repository-card] { [data-component=component-repository-card] {
height: 100%;
.repository-card { .repository-card {
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -12,23 +14,25 @@
flex: 0 0 auto; flex: 0 0 auto;
} }
.header {
display: flex;
width: 100%;
align-items: center;
padding: 10px;
.version-select { .version-select {
position: absolute;
top: 10px;
left: 10px;
width: 9em; width: 9em;
margin-right: auto;
} }
.upgrade-bt { >:not(.version-select) {
position: absolute; font-size: 1.6em;
top: 10px; }
right: 10px;
font-size: 1.8em;
} }
.icon { .icon {
display: flex; display: flex;
padding-top: 50px; padding-top: 10px;
padding-bottom: 20px; padding-bottom: 20px;
color: constants.$production-color; color: constants.$production-color;
font-size: constants.$SIZE_ICON_XL; font-size: constants.$SIZE_ICON_XL;

View File

@@ -1,6 +1,7 @@
@use '@/assets/css/constants' as constants; @use '@/assets/css/constants' as constants;
[data-component=component-store-card] { [data-component=component-store-card] {
height: 100%;
cursor: pointer; cursor: pointer;
.store-card { .store-card {
@@ -14,10 +15,31 @@
flex: 0 0 auto; 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 { .icon {
display: flex; display: flex;
padding-top: 40px; padding-top: 10px;
padding-bottom: 20px; padding-bottom: 20px;
color: constants.$production-color; color: constants.$production-color;
font-size: constants.$SIZE_ICON_XL; font-size: constants.$SIZE_ICON_XL;
@@ -28,12 +50,6 @@
} }
} }
.version {
position: absolute;
left: 10px;
top: 10px;
}
.info { .info {
padding-top: 20px; padding-top: 20px;
@@ -43,8 +59,16 @@
} }
.tool-desc { .tool-desc {
margin-top: 10px; margin: {
top: 10px;
left: auto;
right: auto;
};
color: constants.$font-secondary-color; color: constants.$font-secondary-color;
overflow: hidden;
text-overflow: ellipsis;
max-height: 40px;
width: 80%;
} }
} }
@@ -68,17 +92,16 @@
align-items: center; align-items: center;
} }
} }
}
:hover {
.header {
.version {
opacity: 0;
}
.operation { .operation {
display: flex; opacity: 1;
position: absolute;
top: 10px;
right: 12px;
font-size: 1.6em;
gap: 4px;
> *:hover {
color: constants.$font-secondary-color;
} }
} }
} }

View File

@@ -4,6 +4,13 @@
[data-component=tools-framework] { [data-component=tools-framework] {
.left-panel { .left-panel {
background-color: constants.$origin-color; background-color: constants.$origin-color;
.menu-droppable {
display: flex;
min-height: 0;
flex: 1;
width: 100%;
}
} }
.right-panel { .right-panel {

View File

@@ -11,7 +11,7 @@
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-start; justify-content: flex-start;
> .card-box { > .card-box, > div {
width: 180px; width: 180px;
height: 290px; height: 290px;
flex: 0 0 auto; flex: 0 0 auto;
@@ -77,6 +77,7 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: 20px; gap: 20px;
margin-top: 20px;
:first-child, :last-child { :first-child, :last-child {
height: 0; height: 0;
@@ -99,7 +100,7 @@
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-start; justify-content: flex-start;
> .card-box { > .card-box, > div {
width: 180px; width: 180px;
height: 290px; height: 290px;
flex: 0 0 auto; flex: 0 0 auto;

View File

@@ -26,7 +26,7 @@
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
> .card-box { > div {
width: 180px; width: 180px;
height: 290px; height: 290px;
flex: 0 0 auto; flex: 0 0 auto;

View File

@@ -68,7 +68,7 @@
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
> .card-box { > div {
width: 180px; width: 180px;
height: 290px; height: 290px;
flex: 0 0 auto; flex: 0 0 auto;

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M7 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 2zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 14zm6-8a2 2 0 1 0-.001-4.001A2 2 0 0 0 13 6zm0 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 14z" /></svg>

After

Width:  |  Height:  |  Size: 325 B

View File

@@ -3,10 +3,11 @@ import Icon from '@ant-design/icons'
import Submenu from '@/components/common/Sidebar/Submenu' import Submenu from '@/components/common/Sidebar/Submenu'
type ItemProps = { type ItemProps = {
icon?: IconComponent icon?: IconComponent | string
text?: string text?: string
path: string path: string
children?: ReactNode children?: ReactNode
extend?: ReactNode
end?: boolean end?: boolean
} }
@@ -42,9 +43,19 @@ const Item = (props: ItemProps) => {
} }
> >
<div className={'icon-box'}> <div className={'icon-box'}>
{props.icon && <Icon className={'icon'} component={props.icon} />} {props.icon &&
(typeof props.icon === 'string' ? (
<img
className={'icon'}
src={`data:image/svg+xml;base64,${props.icon}`}
alt={'icon'}
/>
) : (
<Icon className={'icon'} component={props.icon} />
))}
</div> </div>
<span className={'text'}>{props.text}</span> <span className={'text'}>{props.text}</span>
<div className={'extend'}>{props.extend}</div>
</NavLink> </NavLink>
</div> </div>
{props.children && ( {props.children && (

View File

@@ -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 (
<button
data-component={'component-drag-handle'}
style={{ padding }}
ref={ref}
className={'drag-handle'}
{...attributes}
{...listeners}
>
<Icon component={IconOxygenHandle} />
</button>
)
}
export default DragHandle

View File

@@ -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<HandleContext>(
() => ({
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 (
<HandleContextInst.Provider value={context}>
<div ref={draggableRef} style={style}>
{children}
</div>
</HandleContextInst.Provider>
)
}
export default Draggable

View File

@@ -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 <DragOverlay dropAnimation={dropAnimationConfig}>{children}</DragOverlay>
}
export default DraggableOverlay

View File

@@ -0,0 +1,16 @@
import { DetailedHTMLProps, HTMLAttributes } from 'react'
import { useDroppable } from '@dnd-kit/core'
interface DroppableProps extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
id: string
}
const Droppable = ({ id, ...props }: DroppableProps) => {
const { setNodeRef: droppableRef } = useDroppable({
id
})
return <div {...props} ref={droppableRef} />
}
export default Droppable

View File

@@ -0,0 +1,13 @@
import { DraggableSyntheticListeners } from '@dnd-kit/core'
export interface HandleContext {
attributes: Record<string, any>
listeners: DraggableSyntheticListeners
ref(node: HTMLElement | null): void
}
export const HandleContextInst = createContext<HandleContext>({
attributes: {},
listeners: undefined,
ref() {}
})

View File

@@ -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<HandleContext>(
() => ({
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 (
<HandleContextInst.Provider value={context}>
<div
ref={draggableRef}
style={style}
className={isDragging && isDelete ? 'delete' : undefined}
>
{children}
</div>
</HandleContextInst.Provider>
)
}
export default Sortable

View File

@@ -1,14 +1,17 @@
import { DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react' import { DetailedHTMLProps, HTMLAttributes } from 'react'
import VanillaTilt, { TiltOptions } from 'vanilla-tilt' import VanillaTilt, { TiltOptions } from 'vanilla-tilt'
import '@/assets/css/components/tools/repository-card.scss' import '@/assets/css/components/tools/repository-card.scss'
import Card from '@/components/common/Card' import Card from '@/components/common/Card'
import FlexBox from '@/components/common/FlexBox' import FlexBox from '@/components/common/FlexBox'
import Draggable from '@/components/dnd/Draggable'
import DragHandle from '@/components/dnd/DragHandle.tsx'
interface RepositoryCardProps interface RepositoryCardProps
extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> { extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
icon: ReactNode icon: string
toolName: string toolName: string
toolId: string toolId: string
ver: string
options?: TiltOptions options?: TiltOptions
onOpen?: () => void onOpen?: () => void
onEdit?: () => void onEdit?: () => void
@@ -25,6 +28,7 @@ const RepositoryCard = ({
icon, icon,
toolName, toolName,
toolId, toolId,
ver,
options = { options = {
reverse: true, reverse: true,
max: 8, max: 8,
@@ -48,6 +52,7 @@ const RepositoryCard = ({
}, [options]) }, [options])
return ( return (
<Draggable id={toolId} data={{ icon, toolName, toolId, authorUsername: '!', ver }}>
<Card <Card
data-component={'component-repository-card'} data-component={'component-repository-card'}
style={{ overflow: 'visible', ...style }} style={{ overflow: 'visible', ...style }}
@@ -55,10 +60,16 @@ const RepositoryCard = ({
{...props} {...props}
> >
<FlexBox className={'repository-card'}> <FlexBox className={'repository-card'}>
<div className={'icon'}>{icon}</div> <div className={'header'}>
{children}
<DragHandle />
</div>
<div className={'icon'}>
<img src={`data:image/svg+xml;base64,${icon}`} alt={'Icon'} />
</div>
<div className={'info'}> <div className={'info'}>
{toolName && <div className={'tool-name'}>{toolName}</div>} <div className={'tool-name'}>{toolName}</div>
{toolId && <div className={'tool-id'}>{`ID: ${toolId}`}</div>} <div className={'tool-id'}>{`ID: ${toolId}`}</div>
</div> </div>
<div className={'operation'}> <div className={'operation'}>
{onOpen && ( {onOpen && (
@@ -90,9 +101,9 @@ const RepositoryCard = ({
</AntdButton> </AntdButton>
)} )}
</div> </div>
{children}
</FlexBox> </FlexBox>
</Card> </Card>
</Draggable>
) )
} }

View File

@@ -1,4 +1,4 @@
import { DetailedHTMLProps, HTMLAttributes, MouseEvent, ReactNode } from 'react' import { DetailedHTMLProps, HTMLAttributes, MouseEvent } from 'react'
import VanillaTilt, { TiltOptions } from 'vanilla-tilt' import VanillaTilt, { TiltOptions } from 'vanilla-tilt'
import protocolCheck from 'custom-protocol-check' import protocolCheck from 'custom-protocol-check'
import Icon from '@ant-design/icons' 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 Card from '@/components/common/Card'
import FlexBox from '@/components/common/FlexBox' import FlexBox from '@/components/common/FlexBox'
import { getUserId } from '@/util/auth.tsx' import { getUserId } from '@/util/auth.tsx'
import DragHandle from '@/components/dnd/DragHandle.tsx'
import Draggable from '@/components/dnd/Draggable.tsx'
interface StoreCardProps extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> { interface StoreCardProps extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
icon: ReactNode icon: string
toolName: string toolName: string
toolId: string toolId: string
toolDesc: string toolDesc: string
@@ -176,6 +178,10 @@ const StoreCard = ({
return ( return (
<> <>
<Draggable
id={`${author.username}:${toolId}:${ver}`}
data={{ icon, toolName, toolId, authorUsername: author.username, ver: 'latest' }}
>
<Card <Card
data-component={'component-store-card'} data-component={'component-store-card'}
style={{ overflow: 'visible', ...style }} style={{ overflow: 'visible', ...style }}
@@ -184,12 +190,62 @@ const StoreCard = ({
onClick={handleCardOnClick} onClick={handleCardOnClick}
> >
<FlexBox className={'store-card'}> <FlexBox className={'store-card'}>
<div className={'icon'}>{icon}</div> <div className={'header'}>
<div className={'version'}> <div className={'version'}>
<AntdTag> <AntdTag>
{platform.slice(0, 1)}-{ver} {platform.slice(0, 1)}-{ver}
</AntdTag> </AntdTag>
</div> </div>
<div className={'operation'}>
{platform !== 'ANDROID' && supportPlatform.includes('ANDROID') && (
<AntdTooltip title={'Android 端'}>
<Icon
component={IconOxygenMobile}
onClick={handleOnAndroidBtnClick}
/>
</AntdTooltip>
)}
{platform === 'DESKTOP' && supportPlatform.includes('WEB') && (
<AntdTooltip title={'Web 端'}>
<Icon
component={IconOxygenBrowser}
onClick={handleOnWebBtnClick}
/>
</AntdTooltip>
)}
{platform === 'WEB' && supportPlatform.includes('DESKTOP') && (
<AntdTooltip title={'桌面端'}>
<Icon
component={IconOxygenDesktop}
onClick={handleOnDesktopBtnClick}
/>
</AntdTooltip>
)}
<AntdTooltip title={'源码'}>
<Icon
component={IconOxygenCode}
onClick={handleOnSourceBtnClick}
/>
</AntdTooltip>
{author.id !== userId && (
<AntdTooltip title={favorite_ ? '取消收藏' : '收藏'}>
<Icon
component={
favorite_ ? IconOxygenStarFilled : IconOxygenStar
}
style={{
color: favorite_ ? COLOR_PRODUCTION : undefined
}}
onClick={handleOnStarBtnClick}
/>
</AntdTooltip>
)}
<DragHandle />
</div>
</div>
<div className={'icon'}>
<img src={`data:image/svg+xml;base64,${icon}`} alt={'Icon'} />
</div>
<div className={'info'}> <div className={'info'}>
<div className={'tool-name'}>{toolName}</div> <div className={'tool-name'}>{toolName}</div>
<div className={'tool-id'}>{`ID: ${toolId}`}</div> <div className={'tool-id'}>{`ID: ${toolId}`}</div>
@@ -214,43 +270,9 @@ const StoreCard = ({
</AntdTooltip> </AntdTooltip>
</div> </div>
)} )}
<div className={'operation'}>
{platform !== 'ANDROID' && supportPlatform.includes('ANDROID') && (
<AntdTooltip title={'Android 端'}>
<Icon
component={IconOxygenMobile}
onClick={handleOnAndroidBtnClick}
/>
</AntdTooltip>
)}
{platform === 'DESKTOP' && supportPlatform.includes('WEB') && (
<AntdTooltip title={'Web 端'}>
<Icon component={IconOxygenBrowser} onClick={handleOnWebBtnClick} />
</AntdTooltip>
)}
{platform === 'WEB' && supportPlatform.includes('DESKTOP') && (
<AntdTooltip title={'桌面端'}>
<Icon
component={IconOxygenDesktop}
onClick={handleOnDesktopBtnClick}
/>
</AntdTooltip>
)}
<AntdTooltip title={'源码'}>
<Icon component={IconOxygenCode} onClick={handleOnSourceBtnClick} />
</AntdTooltip>
{author.id !== userId && (
<AntdTooltip title={favorite_ ? '取消收藏' : '收藏'}>
<Icon
component={favorite_ ? IconOxygenStarFilled : IconOxygenStar}
style={{ color: favorite_ ? COLOR_PRODUCTION : undefined }}
onClick={handleOnStarBtnClick}
/>
</AntdTooltip>
)}
</div>
</FlexBox> </FlexBox>
</Card> </Card>
</Draggable>
{contextHolder} {contextHolder}
</> </>
) )

View File

@@ -1,7 +1,7 @@
export const PRODUCTION_NAME = 'Oxygen Toolbox' export const PRODUCTION_NAME = 'Oxygen Toolbox'
export const STORAGE_TOKEN_KEY = 'JWT_TOKEN' export const STORAGE_TOKEN_KEY = 'JWT_TOKEN'
export const STORAGE_USER_INFO_KEY = 'USER_INFO' 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_ORIGIN = 'white'
export const COLOR_PRODUCTION = '#4E47BB' export const COLOR_PRODUCTION = '#4E47BB'
export const COLOR_MAIN = COLOR_PRODUCTION export const COLOR_MAIN = COLOR_PRODUCTION

8
src/global.d.ts vendored
View File

@@ -654,3 +654,11 @@ interface ToolFavoriteAddRemoveParam {
toolId: string toolId: string
platform: Platform platform: Platform
} }
interface ToolMenuItem {
icon: string
toolName: string
toolId: string
authorUsername: string
ver: string
}

View File

@@ -134,12 +134,7 @@ const Store = () => {
return ( return (
<StoreCard <StoreCard
key={firstTool!.id} key={firstTool!.id}
icon={ icon={firstTool!.icon}
<img
src={`data:image/svg+xml;base64,${firstTool!.icon}`}
alt={'Icon'}
/>
}
toolName={firstTool!.name} toolName={firstTool!.name}
toolId={firstTool!.toolId} toolId={firstTool!.toolId}
toolDesc={firstTool!.description} toolDesc={firstTool!.description}

View File

@@ -197,12 +197,7 @@ const User = () => {
return ( return (
<StoreCard <StoreCard
key={firstTool!.id} key={firstTool!.id}
icon={ icon={firstTool!.icon}
<img
src={`data:image/svg+xml;base64,${firstTool!.icon}`}
alt={'Icon'}
/>
}
toolName={firstTool!.name} toolName={firstTool!.name}
toolId={firstTool!.toolId} toolId={firstTool!.toolId}
toolDesc={firstTool!.description} toolDesc={firstTool!.description}

View File

@@ -32,8 +32,11 @@ const View = () => {
.compile(files, importMap, toolVo.entryPoint) .compile(files, importMap, toolVo.entryPoint)
.then((result) => { .then((result) => {
const output = result.outputFiles[0].text const output = result.outputFiles[0].text
setCompiledCode('')
setTimeout(() => {
setCompiledCode(`${output}\n${baseDist}`) setCompiledCode(`${output}\n${baseDist}`)
}) })
})
.catch((reason) => { .catch((reason) => {
void message.error(`编译失败:${reason}`) void message.error(`编译失败:${reason}`)
}) })
@@ -72,9 +75,7 @@ const View = () => {
break break
case DATABASE_NO_RECORD_FOUND: case DATABASE_NO_RECORD_FOUND:
void message.error('未找到指定工具') void message.error('未找到指定工具')
setTimeout(() => {
navigateToRepository(navigate) navigateToRepository(navigate)
}, 3000)
break break
default: default:
void message.error('获取工具信息失败,请稍后重试') void message.error('获取工具信息失败,请稍后重试')
@@ -103,11 +104,11 @@ const View = () => {
return return
} }
if (username === '!' && !ver) { if (username === '!' && !ver) {
navigateToView(navigate, '!', toolId!, platform as Platform) navigateToView(navigate, '!', toolId!, platform as Platform, 'latest')
return return
} }
getTool() getTool()
}, []) }, [username, toolId, ver, searchParams])
return ( return (
<FitFullscreen data-component={'tools-view'}> <FitFullscreen data-component={'tools-view'}>

View File

@@ -166,9 +166,10 @@ const ToolCard = ({ tools, onDelete, onUpgrade, onSubmit, onCancel }: ToolCardPr
return ( return (
<RepositoryCard <RepositoryCard
icon={<img src={`data:image/svg+xml;base64,${selectedTool.icon}`} alt={'Icon'} />} icon={selectedTool.icon}
toolName={selectedTool.name} toolName={selectedTool.name}
toolId={selectedTool.toolId} toolId={selectedTool.toolId}
ver={selectedTool.ver}
onOpen={handleOnOpenTool} onOpen={handleOnOpenTool}
onEdit={handleOnEditTool()} onEdit={handleOnEditTool()}
onSource={handleOnSourceTool()} onSource={handleOnSourceTool()}
@@ -563,6 +564,8 @@ const Tools = () => {
))} ))}
{hasNextPage && <LoadMoreCard onClick={handleOnLoadMore} />} {hasNextPage && <LoadMoreCard onClick={handleOnLoadMore} />}
</FlexBox> </FlexBox>
{starToolData.length ? (
<>
<FlexBox className={'favorite-divider'}> <FlexBox className={'favorite-divider'}>
<div /> <div />
<div className={'divider-text'}></div> <div className={'divider-text'}></div>
@@ -574,7 +577,8 @@ const Tools = () => {
if ( if (
!previousValue.some( !previousValue.some(
(value) => (value) =>
value.author.id === currentValue.author.id && value.author.id ===
currentValue.author.id &&
value.toolId === currentValue.toolId value.toolId === currentValue.toolId
) )
) { ) {
@@ -588,7 +592,9 @@ const Tools = () => {
value.author.id === item.author.id && value.author.id === item.author.id &&
value.toolId === item.toolId value.toolId === item.toolId
) )
const webTool = tools.find((value) => value.platform === 'WEB') const webTool = tools.find(
(value) => value.platform === 'WEB'
)
const desktopTool = tools.find( const desktopTool = tools.find(
(value) => value.platform === 'DESKTOP' (value) => value.platform === 'DESKTOP'
) )
@@ -603,25 +609,26 @@ const Tools = () => {
return ( return (
<StoreCard <StoreCard
key={firstTool!.id} key={firstTool!.id}
icon={ icon={firstTool!.icon}
<img
src={`data:image/svg+xml;base64,${firstTool!.icon}`}
alt={'Icon'}
/>
}
toolName={firstTool!.name} toolName={firstTool!.name}
toolId={firstTool!.toolId} toolId={firstTool!.toolId}
toolDesc={firstTool!.description} toolDesc={firstTool!.description}
author={firstTool!.author} author={firstTool!.author}
ver={firstTool!.ver} ver={firstTool!.ver}
platform={firstTool!.platform} platform={firstTool!.platform}
supportPlatform={tools.map((value) => value.platform)} supportPlatform={tools.map(
(value) => value.platform
)}
favorite={firstTool!.favorite} favorite={firstTool!.favorite}
/> />
) )
})} })}
{hasNextStarPage && <LoadMoreCard onClick={handleOnLoadMoreStar} />} {hasNextStarPage && (
<LoadMoreCard onClick={handleOnLoadMoreStar} />
)}
</FlexBox> </FlexBox>
</>
) : undefined}
</FlexBox> </FlexBox>
</HideScrollbar> </HideScrollbar>
</FitFullscreen> </FitFullscreen>

View File

@@ -1,13 +1,90 @@
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 '@/assets/css/pages/tools-framework.scss'
import { tools } from '@/router/tools' import { tools } from '@/router/tools'
import FitFullscreen from '@/components/common/FitFullscreen' import FitFullscreen from '@/components/common/FitFullscreen'
import Sidebar from '@/components/common/Sidebar' import Sidebar from '@/components/common/Sidebar'
import FullscreenLoadingMask from '@/components/common/FullscreenLoadingMask' 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 ToolsFramework = () => {
const [isDelete, setIsDelete] = useState(false)
const [toolMenuItem, setToolMenuItem] = useState<ToolMenuItem[]>(getToolMenuItem)
const [activeItem, setActiveItem] = useState<ToolMenuItem | null>(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 ( return (
<> <>
<FitFullscreen data-component={'tools-framework'} className={'flex-horizontal'}> <FitFullscreen data-component={'tools-framework'} className={'flex-horizontal'}>
<DndContext
onDragStart={handleOnDragStart}
onDragOver={handleOnDragOver}
onDragEnd={handleOnDragEnd}
onDragCancel={handleOnDragCancel}
>
<div className={'left-panel'}> <div className={'left-panel'}>
<Sidebar title={'氧工具'}> <Sidebar title={'氧工具'}>
<Sidebar.ItemList> <Sidebar.ItemList>
@@ -24,34 +101,70 @@ const ToolsFramework = () => {
text={tools[1].name} text={tools[1].name}
/> />
</Sidebar.ItemList> </Sidebar.ItemList>
<Sidebar.Separate style={{ marginBottom: 0 }} /> <Sidebar.Separate />
<Droppable id={'menu'} className={'menu-droppable'}>
<Sidebar.Scroll> <Sidebar.Scroll>
<Sidebar.ItemList> <Sidebar.ItemList>
{tools.map((tool) => { <SortableContext
return tool.menu && items={toolMenuItem.map(
tool.id !== 'tools-store' && ({ authorUsername, toolId, ver }) =>
tool.id !== 'tools-repository' ? ( `${authorUsername}:${toolId}:${ver}`
<Sidebar.Item )}
path={tool.absolutePath} >
icon={tool.icon} {toolMenuItem.map(
text={tool.name} ({
key={tool.id} icon,
toolName,
toolId,
authorUsername,
ver
}) => (
<Sortable
id={`${authorUsername}:${toolId}:${ver}`}
data={{
icon,
toolName,
toolId,
authorUsername,
ver
}}
isDelete={isDelete}
> >
{tool.children &&
tool.children.map((subTool) => {
return (
<Sidebar.Item <Sidebar.Item
path={subTool.absolutePath} path={getViewPath(
text={subTool.name} authorUsername,
key={subTool.id} toolId,
import.meta.env.VITE_PLATFORM,
ver
)}
icon={icon}
text={toolName}
key={`${authorUsername}:${toolId}`}
extend={<DragHandle padding={10} />}
/> />
</Sortable>
) )
})} )}
</Sidebar.Item> </SortableContext>
) : undefined <DraggableOverlay>
})} {activeItem && (
<Sidebar.Item
path={getViewPath(
activeItem.authorUsername,
activeItem.toolId,
import.meta.env.VITE_PLATFORM,
activeItem.ver
)}
icon={activeItem.icon}
text={activeItem.toolName}
key={`${activeItem.authorUsername}:${activeItem.toolId}:${activeItem.ver}`}
extend={<DragHandle padding={10} />}
/>
)}
</DraggableOverlay>
</Sidebar.ItemList> </Sidebar.ItemList>
</Sidebar.Scroll> </Sidebar.Scroll>
</Droppable>
</Sidebar> </Sidebar>
</div> </div>
<div className={'right-panel'}> <div className={'right-panel'}>
@@ -65,6 +178,7 @@ const ToolsFramework = () => {
<Outlet /> <Outlet />
</Suspense> </Suspense>
</div> </div>
</DndContext>
</FitFullscreen> </FitFullscreen>
</> </>
) )

View File

@@ -1,6 +1,8 @@
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import FullscreenLoadingMask from '@/components/common/FullscreenLoadingMask' import FullscreenLoadingMask from '@/components/common/FullscreenLoadingMask'
import { floor } from 'lodash' 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) => { export const randomInt = (start: number, end: number) => {
if (start > end) { 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 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[]
}

View File

@@ -117,3 +117,11 @@ export const navigateToUser = (navigate: NavigateFunction, options?: NavigateOpt
export const navigateToTools = (navigate: NavigateFunction, options?: NavigateOptions) => { export const navigateToTools = (navigate: NavigateFunction, options?: NavigateOptions) => {
navigate('/system/tools', options) navigate('/system/tools', options)
} }
export const getViewPath = (
username: string,
toolId: string,
platform: Platform,
version?: string
) =>
`/view/${username}/${toolId}${version ? `/${version}` : ''}${platform !== import.meta.env.VITE_PLATFORM ? `?platform=${platform}` : ''}`