From 0e804ff7320a84ca5fb5ff9901f732cf45741c05 Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Tue, 30 Jan 2024 13:33:57 +0800 Subject: [PATCH] Add personal tool management page --- src/assets/css/pages/tools/index.scss | 107 ++++++++++++++ src/constants/urls.constants.ts | 1 + src/global.d.ts | 5 +- src/pages/Sign/SignUp.tsx | 4 +- src/pages/Sign/Verify.tsx | 4 +- src/pages/System/Tools/Base.tsx | 18 +-- src/pages/Tools/Create.tsx | 20 +-- src/pages/Tools/View.tsx | 61 ++++++++ src/pages/Tools/index.tsx | 203 +++++++++++++++++++++++++- src/router/tools.tsx | 16 +- src/services/index.tsx | 67 +++++---- src/services/tool.tsx | 12 +- 12 files changed, 457 insertions(+), 61 deletions(-) create mode 100644 src/assets/css/pages/tools/index.scss create mode 100644 src/pages/Tools/View.tsx diff --git a/src/assets/css/pages/tools/index.scss b/src/assets/css/pages/tools/index.scss new file mode 100644 index 0000000..bb85d5a --- /dev/null +++ b/src/assets/css/pages/tools/index.scss @@ -0,0 +1,107 @@ +@use "@/assets/css/mixins" as mixins; +@use '@/assets/css/constants' as constants; + +[data-component=tools] { + .root-content { + padding: 30px; + gap: 20px; + flex-wrap: wrap; + justify-content: flex-start; + + > .card-box { + width: 180px; + height: 290px; + flex: 0 0 auto; + + .common-card { + width: 100%; + height: 100%; + text-align: center; + align-items: center; + + > * { + flex: 0 0 auto; + display: block; + } + + .version-select { + position: absolute; + top: 10px; + left: 10px; + } + + .icon { + display: flex; + padding-top: 50px; + padding-bottom: 20px; + color: constants.$production-color; + font-size: constants.$SIZE_ICON_XL; + justify-content: center; + + img { + width: constants.$SIZE_ICON_XL; + } + } + + .tool-name { + font-weight: bolder; + font-size: 1.6em; + } + } + } + + + & > :first-child { + cursor: pointer; + + .info { + padding-top: 50px; + } + } + + & > :not(:first-child) { + .info { + transform: translateY(20px); + transition: all 0.1s ease; + } + + .operation { + display: flex; + flex: 1; + justify-content: center; + padding-bottom: 20px; + gap: 4px; + width: 70%; + flex-direction: column; + align-items: center; + visibility: hidden; + opacity: 0; + + > *, .edit > * { + width: 100%; + } + + .edit { + > * { + > :first-child { + flex: 1; + } + } + } + } + } + + & > :not(:first-child):hover { + .info { + transform: translateY(-10px); + transition: all 0.2s ease; + } + + .operation { + visibility: visible; + opacity: 1; + transition: all 0.4s ease; + } + } + } +} diff --git a/src/constants/urls.constants.ts b/src/constants/urls.constants.ts index 266a0d3..b285d75 100644 --- a/src/constants/urls.constants.ts +++ b/src/constants/urls.constants.ts @@ -33,6 +33,7 @@ export const URL_SYS_TOOL_TEMPLATE = `${URL_SYS_TOOL}/template` export const URL_TOOL = '/tool' export const URL_TOOL_TEMPLATE = `${URL_TOOL}/template` export const URL_TOOL_CATEGORY = `${URL_TOOL}/category` +export const URL_TOOL_DETAIL = `${URL_TOOL}/detail` export const URL_API_V1 = '/api/v1' export const URL_API_V1_AVATAR_RANDOM_BASE64 = `${URL_API_V1}/avatar/base64` diff --git a/src/global.d.ts b/src/global.d.ts index e0f47e9..1c55dee 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -534,14 +534,12 @@ interface ToolVo { baseId: string author: UserInfoVo ver: string - privately: boolean keywords: string[] categories: ToolCategoryVo[] source: ToolDataVo dist: ToolDataVo - publish: boolean + publish: string review: number - publishTime: string createTime: string updateTime: string } @@ -553,7 +551,6 @@ interface ToolCreateParam { description: string ver: string templateId: string - privately: boolean keywords: string[] categories: string[] } diff --git a/src/pages/Sign/SignUp.tsx b/src/pages/Sign/SignUp.tsx index 0ee88f8..9827d1e 100644 --- a/src/pages/Sign/SignUp.tsx +++ b/src/pages/Sign/SignUp.tsx @@ -103,7 +103,7 @@ const SignUp = () => { return } setIsSending(true) - void message.loading({ content: '发送中', key: 'sending', duration: 0 }) + void message.loading({ content: '发送中', key: 'SENDING', duration: 0 }) void r_auth_resend() .then((res) => { const response = res.data @@ -114,7 +114,7 @@ const SignUp = () => { } }) .finally(() => { - message.destroy('sending') + message.destroy('SENDING') setIsSending(false) }) } diff --git a/src/pages/Sign/Verify.tsx b/src/pages/Sign/Verify.tsx index 049c684..be3ef19 100644 --- a/src/pages/Sign/Verify.tsx +++ b/src/pages/Sign/Verify.tsx @@ -70,7 +70,7 @@ const Verify = () => { return } setIsSending(true) - void message.loading({ content: '发送中', key: 'sending', duration: 0 }) + void message.loading({ content: '发送中', key: 'SENDING', duration: 0 }) void r_auth_resend() .then((res) => { const response = res.data @@ -81,7 +81,7 @@ const Verify = () => { } }) .finally(() => { - message.destroy('sending') + message.destroy('SENDING') setIsSending(false) }) } diff --git a/src/pages/System/Tools/Base.tsx b/src/pages/System/Tools/Base.tsx index 42fef8a..47d3147 100644 --- a/src/pages/System/Tools/Base.tsx +++ b/src/pages/System/Tools/Base.tsx @@ -177,7 +177,7 @@ const Base = () => { } setCompiling(true) setIsLoading(true) - void message.loading({ content: '加载文件中', key: 'compile-loading', duration: 0 }) + void message.loading({ content: '加载文件中', key: 'COMPILE_LOADING', duration: 0 }) if (!baseDetailLoading[value.id]) { getBaseDetail(value) @@ -211,7 +211,7 @@ const Base = () => { baseDetail = prevState[value.id] return prevState }) - message.destroy('compile-loading') + message.destroy('COMPILE_LOADING') const files = base64ToFiles(baseDetail!.source.data!) if (!Object.keys(files).includes(IMPORT_MAP_FILE_NAME)) { void message.warning(`编译中止:未包含 ${IMPORT_MAP_FILE_NAME} 文件`) @@ -263,7 +263,7 @@ const Base = () => { resolve() void message.loading({ content: '编译中', - key: 'compiling', + key: 'COMPILING', duration: 0 }) void compiler @@ -273,14 +273,12 @@ const Base = () => { compileForm.getFieldValue('entryFileName') as string ) .then((result) => { - void message.destroy('compiling') + message.destroy('COMPILING') void message.loading({ content: '上传中', - key: 'uploading', + key: 'UPLOADING', duration: 0 }) - // TODO Remove debug - console.debug(result.outputFiles[0].text) void r_sys_tool_base_update({ id: value.id, dist: strToBase64(result.outputFiles[0].text) @@ -297,14 +295,14 @@ const Base = () => { } }) .finally(() => { - void message.destroy('uploading') + message.destroy('UPLOADING') setCompiling(false) setIsLoading(false) }) }) .catch((e: Error) => { void message.error(`编译失败:${e.message}`) - void message.destroy('compiling') + message.destroy('COMPILING') setCompiling(false) setIsLoading(false) }) @@ -325,7 +323,7 @@ const Base = () => { .catch(() => { setCompiling(false) setIsLoading(false) - message.destroy('compile-loading') + message.destroy('COMPILE_LOADING') }) } } diff --git a/src/pages/Tools/Create.tsx b/src/pages/Tools/Create.tsx index 8a0c87d..b6f6acb 100644 --- a/src/pages/Tools/Create.tsx +++ b/src/pages/Tools/Create.tsx @@ -45,7 +45,7 @@ const Create = () => { ) break case DATABASE_DUPLICATE_KEY: - void message.warning('已存在相同 ID 相同版本的应用') + void message.warning('已存在相同 ID 相同版本或未发布版本的应用') setCreating(false) break default: @@ -71,8 +71,9 @@ const Create = () => { 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() + void form.validateFields(['icon']) }) reader.readAsDataURL(file) @@ -187,7 +188,7 @@ const Create = () => { > ({ validator() { @@ -200,6 +201,7 @@ const Create = () => { } }) ]} + getValueFromEvent={() => {}} > { )} - + { onChange={handleOnTemplateChange} /> - - - { + const navigate = useNavigate() + const [loading, setLoading] = useState(false) + const { username, toolId, ver } = useParams() + + const getTool = () => { + if (loading) { + return + } + setLoading(true) + void message.loading({ content: '加载中……', key: 'LOADING', duration: 0 }) + + void r_tool_detail(username!, toolId!, ver || 'latest') + .then((res) => { + const response = res.data + switch (response.code) { + case DATABASE_SELECT_SUCCESS: + console.log(response.data) + break + case DATABASE_NO_RECORD_FOUND: + void message.error('未找到指定工具') + navigate('/') + break + default: + void message.error('获取工具信息失败,请稍后重试') + } + }) + .finally(() => { + setLoading(false) + message.destroy('LOADING') + }) + } + + useEffect(() => { + if (username === '!' && !getLoginStatus()) { + navigate('/') + return + } + if (username !== '!' && ver) { + navigate(`/view/${username}/${toolId}`) + return + } + if (username === '!' && !ver) { + navigate(`/view/!/${toolId}/latest`) + return + } + getTool() + }, []) + + return ( + <> + {username}:{toolId}:{ver} + + ) +} + +export default View diff --git a/src/pages/Tools/index.tsx b/src/pages/Tools/index.tsx index 0f205de..256313f 100644 --- a/src/pages/Tools/index.tsx +++ b/src/pages/Tools/index.tsx @@ -1,5 +1,206 @@ +import { DetailedHTMLProps, HTMLAttributes, ReactNode, useState } from 'react' +import Icon from '@ant-design/icons' +import VanillaTilt, { TiltOptions } from 'vanilla-tilt' +import '@/assets/css/pages/tools/index.scss' +import FitFullscreen from '@/components/common/FitFullscreen' +import HideScrollbar from '@/components/common/HideScrollbar' +import FlexBox from '@/components/common/FlexBox' +import Card from '@/components/common/Card' +import { r_tool_get } from '@/services/tool.tsx' +import { DATABASE_SELECT_SUCCESS } from '@/constants/common.constants.ts' +import { getLoginStatus } from '@/util/auth.tsx' +import { useNavigate } from 'react-router' + +interface CommonCardProps + extends DetailedHTMLProps, HTMLDivElement> { + icon: ReactNode + toolName?: string + toolId?: string + options?: TiltOptions + url?: string + onOpen?: () => void + onEdit?: () => void + onPublish?: () => void + onDelete?: () => void +} + +const CommonCard = ({ + style, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ref, + icon, + toolName, + toolId, + options = { + reverse: true, + max: 8, + glare: true, + ['max-glare']: 0.3, + scale: 1.03 + }, + url, + onOpen, + onEdit, + onPublish, + onDelete, + children, + ...props +}: CommonCardProps) => { + const navigate = useNavigate() + const cardRef = useRef(null) + + useEffect(() => { + cardRef.current && VanillaTilt.init(cardRef.current, options) + }, [options]) + + const handleCardOnClick = () => { + url && navigate(url) + } + + return ( + + +
{icon}
+
+ {toolName &&
{toolName}
} + {toolId &&
{`ID: ${toolId}`}
} +
+
+ {onOpen && ( + + 打开 + + )} + {onEdit && ( +
+ + 编辑 + {onPublish && 发布} + +
+ )} + {onDelete && ( + + 删除 + + )} +
+ {children} +
+
+ ) +} + +interface ToolCardProps { + tools: ToolVo[] +} + +const ToolCard = ({ tools }: ToolCardProps) => { + const navigate = useNavigate() + const [selectedTool, setSelectedTool] = useState(tools[0]) + + const handleOnVersionChange = (value: string) => { + setSelectedTool(tools.find((item) => item.id === value)!) + } + + const handleOnOpenTool = () => { + navigate(`/view/!/${selectedTool.toolId}/${selectedTool.ver}`) + } + + const handleOnEditTool = () => {} + + const handleOnPublishTool = () => {} + + const handleOnDeleteTool = () => {} + + return ( + } + toolName={selectedTool.name} + toolId={selectedTool.toolId} + onOpen={handleOnOpenTool} + onEdit={handleOnEditTool} + onPublish={handleOnPublishTool} + onDelete={handleOnDeleteTool} + > + ({ + value: value.id, + label: `${value.ver}${value.publish === '0' ? '*' : ''}` + }))} + /> + + ) +} + const Tools = () => { - return <> + const [loading, setLoading] = useState(false) + const [toolData, setToolData] = useState() + + const getTool = () => { + if (loading) { + return + } + setLoading(true) + void message.loading({ content: '加载工具列表中', key: 'LOADING', duration: 0 }) + + void r_tool_get() + .then((res) => { + const response = res.data + + switch (response.code) { + case DATABASE_SELECT_SUCCESS: + setToolData(response.data!) + break + default: + void message.error('获取工具失败,请稍后重试') + } + }) + .finally(() => { + setLoading(false) + message.destroy('LOADING') + }) + } + + useEffect(() => { + if (!getLoginStatus()) { + return + } + getTool() + }, []) + + return ( + <> + + + + } + toolName={'创建工具'} + url={'/create'} + /> + {toolData && + Object.values( + toolData.reduce((result: Record, item) => { + result[item.toolId] = result[item.toolId] || [] + result[item.toolId].push(item) + return result + }, {}) + ).map((value, index) => )} + + + + + ) } export default Tools diff --git a/src/router/tools.tsx b/src/router/tools.tsx index 30c6ada..7c16e4f 100644 --- a/src/router/tools.tsx +++ b/src/router/tools.tsx @@ -28,9 +28,23 @@ export const tools: RouteJsonObject[] = [ name: '创建工具', titlePostfix: ' - 创建新工具', icon: lazy(() => import('~icons/oxygen/newProject')), - menu: true, + menu: false, auth: true }, + { + path: 'view/:username/:toolId/:ver', + absolutePath: '/view', + id: 'tools-view-ver', + component: lazy(() => import('@/pages/Tools/View')), + name: '查看' + }, + { + path: 'view/:username/:toolId', + absolutePath: '/view', + id: 'tools-view', + component: lazy(() => import('@/pages/Tools/View')), + name: '查看' + }, { path: '*', absolutePath: '*', diff --git a/src/services/index.tsx b/src/services/index.tsx index c4652b7..149a65e 100644 --- a/src/services/index.tsx +++ b/src/services/index.tsx @@ -68,11 +68,14 @@ service.interceptors.response.use( switch (response.data.code) { case PERMISSION_UNAUTHORIZED: removeToken() - void message.error( - <> - 未登录 - - ) + void message.error({ + content: ( + <> + 未登录 + + ), + key: 'NO_LOGIN' + }) setTimeout(() => { location.reload() }, 1500) @@ -80,11 +83,14 @@ service.interceptors.response.use( case PERMISSION_TOKEN_ILLEGAL: case PERMISSION_TOKEN_HAS_EXPIRED: removeToken() - void message.error( - <> - 登录已过期 - - ) + void message.error({ + content: ( + <> + 登录已过期 + + ), + key: 'LOGIN_HAS_EXPIRED' + }) setTimeout(() => { location.replace( getRedirectUrl('/login', `${location.pathname}${location.search}`) @@ -92,18 +98,24 @@ service.interceptors.response.use( }, 1500) throw response?.data case PERMISSION_ACCESS_DENIED: - void message.error( - <> - 暂无权限操作 - - ) + void message.error({ + content: ( + <> + 暂无权限操作 + + ), + key: 'ACCESS_DENIED' + }) throw response?.data case SYSTEM_REQUEST_TOO_FREQUENT: - void message.warning( - <> - 请求过于频繁,请稍后重试 - - ) + void message.warning({ + content: ( + <> + 请求过于频繁,请稍后重试 + + ), + key: 'REQUEST_TOO_FREQUENT' + }) throw response?.data } return response @@ -113,13 +125,16 @@ service.interceptors.response.use( error.code === 'ETIMEDOUT' || (error.code === 'ECONNABORTED' && error.message.includes('timeout')) ) { - void message.error('请求超时,请稍后重试') + void message.error({ content: '请求超时,请稍后重试', key: 'TIMEOUT' }) } else { - void message.error( - <> - 服务器出错,请稍后重试 - - ) + void message.error({ + content: ( + <> + 服务器出错,请稍后重试 + + ), + key: 'SERVER_ERROR' + }) } return await Promise.reject(error?.response?.data) } diff --git a/src/services/tool.tsx b/src/services/tool.tsx index 7621345..705fd7c 100644 --- a/src/services/tool.tsx +++ b/src/services/tool.tsx @@ -1,5 +1,10 @@ import request from '@/services/index' -import { URL_TOOL, URL_TOOL_CATEGORY, URL_TOOL_TEMPLATE } from '@/constants/urls.constants' +import { + URL_TOOL, + URL_TOOL_CATEGORY, + URL_TOOL_DETAIL, + URL_TOOL_TEMPLATE +} from '@/constants/urls.constants' export const r_tool_template_get = () => request.get(URL_TOOL_TEMPLATE) @@ -9,3 +14,8 @@ export const r_tool_template_get_one = (id: string) => export const r_tool_category_get = () => request.get(URL_TOOL_CATEGORY) export const r_tool_create = (param: ToolCreateParam) => request.post(URL_TOOL, param) + +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}`)