diff --git a/build/resolvers/antd.ts b/build/resolvers/antd.ts index 3d18000..f0d1b45 100644 --- a/build/resolvers/antd.ts +++ b/build/resolvers/antd.ts @@ -295,6 +295,7 @@ const primitiveNames = [ 'DropdownButton', 'Drawer', 'Empty', + 'FloatButton', 'Form', 'FormItem', 'FormItemRest', diff --git a/package-lock.json b/package-lock.json index 0c6b21d..4a95697 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "monaco-jsx-syntax-highlight": "^1.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-draggable": "^4.4.6", "react-router": "^6.21.3", "react-router-dom": "^6.21.3", "size-sensor": "^1.0.2", @@ -2756,6 +2757,14 @@ "resolved": "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz", "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -5283,6 +5292,14 @@ "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", "dev": true }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.12.3.tgz", @@ -5644,6 +5661,21 @@ "node": ">=6.0.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -6287,6 +6319,19 @@ "react": "^18.2.0" } }, + "node_modules/react-draggable": { + "version": "4.4.6", + "resolved": "https://registry.npmmirror.com/react-draggable/-/react-draggable-4.4.6.tgz", + "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", diff --git a/package.json b/package.json index d9bdd2d..c96b4e2 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "monaco-jsx-syntax-highlight": "^1.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-draggable": "^4.4.6", "react-router": "^6.21.3", "react-router-dom": "^6.21.3", "size-sensor": "^1.0.2", diff --git a/src/assets/css/pages/tools/edit.scss b/src/assets/css/pages/tools/edit.scss index fb79eda..2693826 100644 --- a/src/assets/css/pages/tools/edit.scss +++ b/src/assets/css/pages/tools/edit.scss @@ -2,5 +2,27 @@ .root-content { width: 100%; height: 100%; + + > * { + width: 0; + } + + .draggable-mask { + position: absolute; + width: 100%; + height: 100%; + } + } + + .draggable-content { + position: fixed; + inset-inline-end: 32px; + inset-block-end: 48px; + + > * { + position: relative; + inset-inline-end: 0; + inset-block-end: 0; + } } } \ No newline at end of file diff --git a/src/components/common/HideScrollbar.tsx b/src/components/common/HideScrollbar.tsx index be9dfc5..ed12cbf 100644 --- a/src/components/common/HideScrollbar.tsx +++ b/src/components/common/HideScrollbar.tsx @@ -458,26 +458,48 @@ const HideScrollbar = forwardRef( useEffect(() => { rootRef.current?.removeEventListener('wheel', wheelListenerRef.current) - if (isPreventAnyScroll) { - const handleDefaultWheel = (event: WheelEvent) => { - if (!event.altKey && !event.ctrlKey) { - if (isPreventScroll) { - event.preventDefault() - return - } - if (isPreventVerticalScroll && !event.shiftKey && !event.deltaX) { - event.preventDefault() - return - } - if (isPreventHorizontalScroll && (event.shiftKey || !event.deltaY)) { - event.preventDefault() - return - } + const handleDefaultWheel = (event: WheelEvent) => { + if (!event.altKey && !event.ctrlKey) { + if (isPreventScroll) { + event.preventDefault() + return + } + if ( + isPreventVerticalScroll && + verticalScrollbarLength < 100 && + !event.shiftKey && + !event.deltaX + ) { + event.preventDefault() + return + } + if (isPreventHorizontalScroll && (event.shiftKey || !event.deltaY)) { + event.preventDefault() + return + } + let length = verticalScrollbarLength + setVerticalScrollbarLength((prevState) => { + length = prevState + return prevState + }) + console.log(length) + if ( + !isPreventHorizontalScroll && + length >= 100 && + !event.shiftKey && + !event.deltaX + ) { + event.preventDefault() + rootRef.current?.scrollTo({ + left: rootRef.current?.scrollLeft + event.deltaY, + behavior: 'smooth' + }) + return } } - wheelListenerRef.current = handleDefaultWheel - rootRef.current?.addEventListener('wheel', handleDefaultWheel, { passive: false }) } + wheelListenerRef.current = handleDefaultWheel + rootRef.current?.addEventListener('wheel', handleDefaultWheel, { passive: false }) }, [ isPreventAnyScroll, isPreventHorizontalScroll, diff --git a/src/constants/common.constants.ts b/src/constants/common.constants.ts index 12cc77e..958fb05 100644 --- a/src/constants/common.constants.ts +++ b/src/constants/common.constants.ts @@ -75,6 +75,9 @@ export const DATABASE_DUPLICATE_KEY = 30051 export const DATABASE_NO_RECORD_FOUND = 30052 export const TOOL_ILLEGAL_VERSION = 40050 +export const TOOL_UNDER_REVIEW = 40051 +export const TOOL_HAS_UNPUBLISHED_VERSION = 40052 +export const TOOL_HAS_BEEN_PUBLISHED = 40053 export const API_AVATAR_SUCCESS = 50100 export const API_AVATAR_ERROR = 50150 diff --git a/src/global.d.ts b/src/global.d.ts index 481d5f7..1602afc 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -560,3 +560,13 @@ interface ToolUpgradeParam { toolId: string ver: string } + +interface ToolUpdateParam { + id: string + name?: string + icon?: string + description?: string + keywords?: string[] + categories?: string[] + source?: string +} diff --git a/src/pages/Tools/Edit.tsx b/src/pages/Tools/Edit.tsx index 63ef259..f6079f8 100644 --- a/src/pages/Tools/Edit.tsx +++ b/src/pages/Tools/Edit.tsx @@ -1,22 +1,32 @@ import '@/assets/css/pages/tools/edit.scss' -import { r_tool_detail } from '@/services/tool.tsx' -import { DATABASE_NO_RECORD_FOUND, DATABASE_SELECT_SUCCESS } from '@/constants/common.constants.ts' -import { useEffect, useState } from 'react' -import Playground from '@/components/Playground' -import FitFullscreen from '@/components/common/FitFullscreen.tsx' -import FlexBox from '@/components/common/FlexBox.tsx' -import { IFiles, IImportMap, ITsconfig } from '@/components/Playground/shared.ts' +import Draggable from 'react-draggable' +import Icon from '@ant-design/icons' +import { + DATABASE_NO_RECORD_FOUND, + DATABASE_SELECT_SUCCESS, + DATABASE_UPDATE_SUCCESS, + TOOL_HAS_BEEN_PUBLISHED, + TOOL_UNDER_REVIEW +} from '@/constants/common.constants' +import { r_tool_category_get, r_tool_detail, r_tool_update } from '@/services/tool' +import { IFiles, IImportMap, ITsconfig } from '@/components/Playground/shared' import { base64ToFiles, base64ToStr, + filesToBase64, IMPORT_MAP_FILE_NAME, TS_CONFIG_FILE_NAME -} from '@/components/Playground/files.ts' -import LoadingMask from '@/components/common/LoadingMask.tsx' +} from '@/components/Playground/files' +import Playground from '@/components/Playground' +import FitFullscreen from '@/components/common/FitFullscreen' +import FlexBox from '@/components/common/FlexBox' +import LoadingMask from '@/components/common/LoadingMask' const Edit = () => { const navigate = useNavigate() const { toolId } = useParams() + const [form] = AntdForm.useForm() + const formValues = AntdForm.useWatch([], form) const [loading, setLoading] = useState(false) const [toolData, setToolData] = useState() const [files, setFiles] = useState({}) @@ -27,8 +37,17 @@ const Edit = () => { const [tsconfig, setTsconfig] = useState() const [entryPoint, setEntryPoint] = useState('') const [baseDist, setBaseDist] = useState('') + const [showDraggableMask, setShowDraggableMask] = useState(false) + const [isDrawerOpen, setIsDrawerOpen] = useState(false) + const [submittable, setSubmittable] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + const [hasEdited, setHasEdited] = useState(false) + const [categoryData, setCategoryData] = useState() + const [loadingCategory, setLoadingCategory] = useState(false) const handleOnChangeFileContent = (content: string, fileName: string, files: IFiles) => { + setHasEdited(true) + if (fileName === IMPORT_MAP_FILE_NAME) { setImportMapRaw(content) return @@ -38,11 +57,144 @@ const Edit = () => { return } - delete files[IMPORT_MAP_FILE_NAME] - delete files[TS_CONFIG_FILE_NAME] setFiles(files) } + const handleOnSetting = () => { + setIsDrawerOpen(true) + form.setFieldValue('icon', toolData?.icon) + form.setFieldValue('name', toolData?.name) + form.setFieldValue('description', toolData?.description) + form.setFieldValue('keywords', toolData?.keywords) + form.setFieldValue( + 'categories', + toolData?.categories.map((value) => value.id) + ) + if (!categoryData || !categoryData.length) { + getCategory() + } + void form.validateFields() + } + + const handleOnSave = () => { + if (isSubmitting) { + return + } + setIsSubmitting(true) + void message.loading({ content: '保存中', key: 'SAVING', duration: 0 }) + + void r_tool_update({ + id: toolData!.id, + source: filesToBase64(files) + }) + .then((res) => { + const response = res.data + switch (response.code) { + case DATABASE_UPDATE_SUCCESS: + void message.success('保存成功') + getTool() + break + case TOOL_UNDER_REVIEW: + void message.error('保存失败:工具审核中') + navigate('/') + break + case TOOL_HAS_BEEN_PUBLISHED: + void message.error('保存失败:工具已发布') + navigate('/') + break + default: + void message.error('保存失败,请稍后重试') + } + }) + .finally(() => { + setIsSubmitting(false) + message.destroy('SAVING') + }) + } + + const handleOnDrawerClose = () => { + setIsDrawerOpen(false) + } + + const handleOnIconBeforeUpload = ( + file: Parameters<_GetProp<_UploadProps, 'beforeUpload'>>[0] + ) => { + if (file.type !== 'image/svg+xml') { + void message.error('仅支持 svg 文件') + return false + } + if (file.size / 1024 / 1024 > 2) { + void message.error('文件大小不能大于2MiB') + } + + const reader = new FileReader() + reader.addEventListener('load', () => { + // eslint-disable-next-line @typescript-eslint/no-base-to-string + form.setFieldValue('icon', reader.result!.toString().split(',')[1]) + void form.validateFields(['icon']) + }) + reader.readAsDataURL(file) + + return false + } + + const handleOnSubmit = () => { + if (isSubmitting) { + return + } + setIsSubmitting(true) + + void r_tool_update({ + ...formValues, + id: toolData!.id + }) + .then((res) => { + const response = res.data + switch (response.code) { + case DATABASE_UPDATE_SUCCESS: + setIsDrawerOpen(false) + void message.success('保存成功') + getTool() + break + case TOOL_UNDER_REVIEW: + void message.error('保存失败:工具审核中') + navigate('/') + break + case TOOL_HAS_BEEN_PUBLISHED: + void message.error('保存失败:工具已发布') + navigate('/') + break + default: + void message.error('保存失败,请稍后重试') + } + }) + .finally(() => { + setIsSubmitting(false) + }) + } + + const getCategory = () => { + if (loadingCategory) { + return + } + setLoadingCategory(true) + + void r_tool_category_get() + .then((res) => { + const response = res.data + switch (response.code) { + case DATABASE_SELECT_SUCCESS: + setCategoryData(response.data!) + break + default: + void message.error('获取类别列表失败,请稍后重试') + } + }) + .finally(() => { + setLoadingCategory(false) + }) + } + const getTool = () => { if (loading) { return @@ -59,6 +211,7 @@ const Edit = () => { case 'NONE': case 'REJECT': setToolData(response.data!) + setHasEdited(false) break case 'PROCESSING': void message.warning('工具审核中,请勿修改') @@ -114,54 +267,195 @@ const Edit = () => { setEntryPoint(toolData.entryPoint) setTimeout(() => { setSelectedFileName(toolData.entryPoint) - }, 100) + }, 500) } catch (e) { console.error(e) void message.error('载入工具失败') } }, [toolData]) + useEffect(() => { + form.validateFields({ validateOnly: true }).then( + () => { + setSubmittable(true) + }, + () => { + setSubmittable(false) + } + ) + }, [formValues]) + useEffect(() => { getTool() }, []) - return ( - - - - - + return Promise.resolve() + } + }) + ]} + getValueFromEvent={() => {}} + > + + {formValues?.icon ? ( + {'icon'} + ) : ( + + )} + + + + + + + + + + + + + + ({ + value: value.id, + label: value.name + }))} + loading={loadingCategory} + disabled={loadingCategory} + /> + + + ) + + return ( + <> + + + + {showDraggableMask &&
} + + setShowDraggableMask(true)} + onStop={() => setShowDraggableMask(false)} + bounds={'#root'} + > +
+ {hasEdited ? ( + } + onClick={handleOnSave} + /> + ) : ( + } + onClick={handleOnSetting} + /> + )} +
+
+ + + {editForm} + + ) } diff --git a/src/pages/Tools/index.tsx b/src/pages/Tools/index.tsx index b89ff28..331cf38 100644 --- a/src/pages/Tools/index.tsx +++ b/src/pages/Tools/index.tsx @@ -6,7 +6,9 @@ import { DATABASE_DELETE_SUCCESS, DATABASE_SELECT_SUCCESS, DATABASE_UPDATE_SUCCESS, - TOOL_ILLEGAL_VERSION + TOOL_HAS_UNPUBLISHED_VERSION, + TOOL_ILLEGAL_VERSION, + TOOL_UNDER_REVIEW } from '@/constants/common.constants' import { getLoginStatus } from '@/util/auth' import { r_tool_delete, r_tool_get, r_tool_upgrade } from '@/services/tool' @@ -266,14 +268,23 @@ const Tools = () => { switch (response.code) { case DATABASE_UPDATE_SUCCESS: void message.success('创建新版本成功') - const toolVo = response.data! - navigate(`/view/!/${toolVo.toolId}/${toolVo.ver}`) + navigate( + `/view/!/${response.data!.toolId}/${response.data!.ver}` + ) resolve() break case TOOL_ILLEGAL_VERSION: void message.error('版本有误,请重新输入') reject() break + case TOOL_UNDER_REVIEW: + void message.error('更新失败:工具审核中') + resolve() + break + case TOOL_HAS_UNPUBLISHED_VERSION: + void message.error('更新失败:存在未发布版本') + resolve() + break default: void message.error('更新失败,请稍后重试') reject() diff --git a/src/services/tool.tsx b/src/services/tool.tsx index 7339843..9f7da61 100644 --- a/src/services/tool.tsx +++ b/src/services/tool.tsx @@ -22,4 +22,6 @@ export const r_tool_get = () => request.get(URL_TOOL) export const r_tool_detail = (username: string, toolId: string, ver: string) => request.get(`${URL_TOOL_DETAIL}/${username}/${toolId}/${ver}`) +export const r_tool_update = (param: ToolUpdateParam) => request.put(URL_TOOL, param) + export const r_tool_delete = (id: string) => request.delete(`${URL_TOOL}/${id}`)