diff --git a/src/assets/css/pages/tools/store.scss b/src/assets/css/pages/tools/store.scss index d1a6d13..2028cec 100644 --- a/src/assets/css/pages/tools/store.scss +++ b/src/assets/css/pages/tools/store.scss @@ -132,5 +132,13 @@ } } } + + .no-tool { + display: flex; + justify-content: center; + font-size: 1.4em; + font-weight: bolder; + color: constants.$font-secondary-color; + } } } \ No newline at end of file diff --git a/src/assets/css/pages/tools/user.scss b/src/assets/css/pages/tools/user.scss new file mode 100644 index 0000000..be5635e --- /dev/null +++ b/src/assets/css/pages/tools/user.scss @@ -0,0 +1,188 @@ +@use '@/assets/css/constants' as constants; + +[data-component=tools-store-user] .root-content { + padding: { + top: 80px; + left: 30px; + right: 30px; + bottom: 30px; + }; + + .root-box { + width: 100%; + height: 100%; + overflow: visible; + align-items: center; + min-width: 900px; + padding-bottom: 20px; + + > .info { + margin-left: 40px; + transform: translateY(-40px); + + > * { + flex: 0 0 auto; + } + + .avatar-box { + background-color: white; + padding: 4px; + border-radius: 50%; + box-shadow: 5px 5px 15px 0 rgba(0, 0, 0, 0.1); + + .avatar { + background-color: transparent !important; + } + } + + .info-name { + margin: { + top: 20px; + left: 24px; + }; + justify-content: center; + + > * { + flex: 0 0 auto; + } + + .nickname { + font-size: 2.4em; + font-weight: bolder; + color: constants.$production-color; + } + + .url { + cursor: pointer; + + > span { + margin-left: 8px; + } + } + } + } + + .tools { + padding: 20px; + gap: 20px; + flex-wrap: wrap; + justify-content: center; + + > .card-box { + width: 180px; + height: 290px; + flex: 0 0 auto; + cursor: pointer; + + .common-card { + width: 100%; + height: 100%; + text-align: center; + align-items: center; + + > * { + display: block; + flex: 0 0 auto; + } + + + .icon { + display: flex; + padding-top: 40px; + padding-bottom: 20px; + color: constants.$production-color; + font-size: constants.$SIZE_ICON_XL; + justify-content: center; + + img { + width: constants.$SIZE_ICON_XL; + } + } + + .version { + position: absolute; + left: 10px; + top: 10px; + } + + .info { + padding-top: 20px; + + .tool-name { + font-weight: bolder; + font-size: 1.6em; + } + + .tool-desc { + margin-top: 10px; + color: constants.$font-secondary-color; + } + } + + .author { + display: flex; + margin-top: auto; + flex-direction: row; + justify-content: end; + padding-bottom: 10px; + gap: 10px; + + .avatar { + > * { + width: 24px; + height: 24px; + } + } + + .author-name { + display: flex; + align-items: center; + } + } + + .operation { + position: absolute; + top: 6px; + right: 12px; + font-size: 1.6em; + + > *:hover { + color: constants.$font-secondary-color; + } + } + } + + .load-more-card { + width: 100%; + height: 100%; + text-align: center; + align-items: center; + + .icon { + display: flex; + font-size: constants.$SIZE_ICON_XXL; + color: constants.$production-color; + align-items: center; + transform: translateY(-20px); + } + + .text { + position: absolute; + top: 60%; + font-size: 1.2em; + font-weight: bolder; + } + } + } + + .no-tool { + display: flex; + justify-content: center; + margin-bottom: 20px; + font-size: 1.2em; + font-weight: bolder; + color: constants.$font-secondary-color; + } + } + } +} \ No newline at end of file diff --git a/src/assets/svg/copy.svg b/src/assets/svg/copy.svg new file mode 100644 index 0000000..0b71764 --- /dev/null +++ b/src/assets/svg/copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/pages/System/Tools/Execute.tsx b/src/pages/System/Tools/Execute.tsx index 1b13b1b..8099d32 100644 --- a/src/pages/System/Tools/Execute.tsx +++ b/src/pages/System/Tools/Execute.tsx @@ -1,12 +1,12 @@ -import FitFullscreen from '@/components/common/FitFullscreen.tsx' import '@/assets/css/pages/system/tools/execute.scss' -import { base64ToFiles, base64ToStr, IMPORT_MAP_FILE_NAME } from '@/components/Playground/files.ts' -import { IImportMap } from '@/components/Playground/shared.ts' -import compiler from '@/components/Playground/compiler.ts' -import { DATABASE_NO_RECORD_FOUND, DATABASE_SELECT_SUCCESS } from '@/constants/common.constants.ts' -import { r_sys_tool_get_one } from '@/services/system.tsx' -import Card from '@/components/common/Card.tsx' +import { DATABASE_NO_RECORD_FOUND, DATABASE_SELECT_SUCCESS } from '@/constants/common.constants' +import { r_sys_tool_get_one } from '@/services/system' +import FitFullscreen from '@/components/common/FitFullscreen' +import Card from '@/components/common/Card' import Playground from '@/components/Playground' +import compiler from '@/components/Playground/compiler' +import { IImportMap } from '@/components/Playground/shared' +import { base64ToFiles, base64ToStr, IMPORT_MAP_FILE_NAME } from '@/components/Playground/files' const Execute = () => { const navigate = useNavigate() diff --git a/src/pages/System/Tools/index.tsx b/src/pages/System/Tools/index.tsx index fc0e123..26b0aa4 100644 --- a/src/pages/System/Tools/index.tsx +++ b/src/pages/System/Tools/index.tsx @@ -1,4 +1,5 @@ import { ChangeEvent, KeyboardEvent } from 'react' +import Icon from '@ant-design/icons' import { r_sys_tool_delete, r_sys_tool_get, @@ -15,16 +16,15 @@ import { DATABASE_SELECT_SUCCESS, DATABASE_UPDATE_SUCCESS, TOOL_NOT_UNDER_REVIEW -} from '@/constants/common.constants.ts' -import FlexBox from '@/components/common/FlexBox.tsx' -import Card from '@/components/common/Card.tsx' -import FitFullscreen from '@/components/common/FitFullscreen.tsx' -import HideScrollbar from '@/components/common/HideScrollbar.tsx' -import Icon from '@ant-design/icons' -import compiler from '@/components/Playground/compiler.ts' -import { base64ToFiles, IMPORT_MAP_FILE_NAME, strToBase64 } from '@/components/Playground/files.ts' -import { IImportMap } from '@/components/Playground/shared.ts' -import Permission from '@/components/common/Permission.tsx' +} from '@/constants/common.constants' +import FlexBox from '@/components/common/FlexBox' +import Card from '@/components/common/Card' +import FitFullscreen from '@/components/common/FitFullscreen' +import HideScrollbar from '@/components/common/HideScrollbar' +import compiler from '@/components/Playground/compiler' +import { IImportMap } from '@/components/Playground/shared' +import { base64ToFiles, IMPORT_MAP_FILE_NAME, strToBase64 } from '@/components/Playground/files' +import Permission from '@/components/common/Permission' const Tools = () => { const navigate = useNavigate() diff --git a/src/pages/Tools/Source.tsx b/src/pages/Tools/Source.tsx index 399bf51..0c79b7b 100644 --- a/src/pages/Tools/Source.tsx +++ b/src/pages/Tools/Source.tsx @@ -6,7 +6,7 @@ import { base64ToFiles } from '@/components/Playground/files' import Playground from '@/components/Playground' import FitFullscreen from '@/components/common/FitFullscreen' import Card from '@/components/common/Card' -import { getLoginStatus } from '@/util/auth.tsx' +import { getLoginStatus } from '@/util/auth' const Source = () => { const navigate = useNavigate() diff --git a/src/pages/Tools/Store.tsx b/src/pages/Tools/Store.tsx index 5f330df..5de53ab 100644 --- a/src/pages/Tools/Store.tsx +++ b/src/pages/Tools/Store.tsx @@ -1,4 +1,4 @@ -import { DetailedHTMLProps, HTMLAttributes, MouseEvent, ReactNode, UIEvent, useState } from 'react' +import { DetailedHTMLProps, HTMLAttributes, MouseEvent, ReactNode, UIEvent } from 'react' import VanillaTilt, { TiltOptions } from 'vanilla-tilt' import Icon from '@ant-design/icons' import '@/assets/css/pages/tools/store.scss' @@ -56,6 +56,11 @@ const CommonCard = ({ url && navigate(url) } + const handleOnClickAuthor = (e: MouseEvent) => { + e.stopPropagation() + navigate(authorUsername) + } + const handleOnSourceBtnClick = (e: MouseEvent) => { e.stopPropagation() navigate(`/source/${authorUsername}/${toolId}`) @@ -78,12 +83,12 @@ const CommonCard = ({ {`ID: ${toolId}`} {toolDesc && {`简介:${toolDesc}`}} - + }} + preview={false} src={`data:image/png;base64,${authorAvatar}`} alt={'Avatar'} /> @@ -139,11 +144,13 @@ const Store = () => { const scrollTopRef = useRef(0) const [isLoading, setIsLoading] = useState(false) const [currentPage, setCurrentPage] = useState(0) - const [hasNextPage, setHasNextPage] = useState(true) + const [hasNextPage, setHasNextPage] = useState(false) const [toolData, setToolData] = useState([]) const [hideSearch, setHideSearch] = useState(false) + const [searchValue, setSearchValue] = useState('') const handleOnSearch = (value: string) => { + setSearchValue(value) getTool(1, value) } @@ -160,7 +167,7 @@ const Store = () => { if (isLoading) { return } - getTool(currentPage + 1, '') + getTool(currentPage + 1, searchValue) } const getTool = (page: number, searchValue: string) => { @@ -177,7 +184,10 @@ const Store = () => { switch (response.code) { case DATABASE_SELECT_SUCCESS: setCurrentPage(response.data!.current) - if (response.data!.current === response.data!.pages) { + if ( + response.data!.current === response.data!.pages || + response.data!.total === 0 + ) { setHasNextPage(false) } else { setHasNextPage(true) @@ -220,6 +230,7 @@ const Store = () => { /> + {!toolData.length && 未找到任何工具} {toolData?.map((value) => ( , HTMLDivElement> { + icon: ReactNode + toolName: string + toolId: string + toolDesc: string + options?: TiltOptions + url: string + authorUsername: string + ver: string +} + +const CommonCard = ({ + style, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ref, + icon, + toolName, + toolId, + toolDesc, + options = { + reverse: true, + max: 8, + glare: true, + ['max-glare']: 0.3, + scale: 1.03 + }, + url, + authorUsername, + ver, + ...props +}: CommonCardProps) => { + const navigate = useNavigate() + const cardRef = useRef(null) + + useEffect(() => { + cardRef.current && VanillaTilt.init(cardRef.current, options) + }, [options]) + + const handleCardOnClick = () => { + url && navigate(url) + } + + const handleOnSourceBtnClick = (e: MouseEvent) => { + e.stopPropagation() + navigate(`/source/${authorUsername}/${toolId}`) + } + + return ( + + + {icon} + + V{ver} + + + {toolName} + {`ID: ${toolId}`} + {toolDesc && {`简介:${toolDesc}`}} + + + + + + + + + ) +} + +interface LoadMoreCardProps { + onClick: () => void +} + +const LoadMoreCard = ({ onClick }: LoadMoreCardProps) => { + const cardRef = useRef(null) + + useEffect(() => { + cardRef.current && + VanillaTilt.init(cardRef.current, { + reverse: true, + max: 8, + glare: true, + ['max-glare']: 0.3, + scale: 1.03 + }) + }, []) + + return ( + + + + {' '} + + 加载更多 + + + ) +} + +const User = () => { + const { username } = useParams() + const navigate = useNavigate() + const [isLoading, setIsLoading] = useState(false) + const [userWithInfoVo, setUserWithInfoVo] = useState() + const [isLoadingTools, setIsLoadingTools] = useState(false) + const [currentPage, setCurrentPage] = useState(0) + const [hasNextPage, setHasNextPage] = useState(false) + const [toolData, setToolData] = useState([]) + + const handleOnCopyToClipboard = (username?: string) => { + return username + ? () => { + void navigator.clipboard + .writeText(new URL(`/store/${username}`, location.href).href) + .then(() => { + void message.success('已复制到剪切板') + }) + } + : undefined + } + + const getProfile = () => { + if (isLoading) { + return + } + setIsLoading(true) + void message.loading({ content: '加载中', key: 'LOADING', duration: 0 }) + + void r_sys_user_info_get_basic(username!) + .then((res) => { + const response = res.data + switch (response.code) { + case DATABASE_SELECT_SUCCESS: + setUserWithInfoVo(response.data!) + getTool(1) + break + case DATABASE_NO_RECORD_FOUND: + void message.warning('用户不存在') + setTimeout(() => { + navigate(-1) + }, 3000) + break + default: + void message.error('获取失败请稍后重试') + } + }) + .finally(() => { + setIsLoading(false) + void message.destroy('LOADING') + }) + } + + const handleOnLoadMore = () => { + if (isLoading) { + return + } + getTool(currentPage + 1) + } + + const getTool = (page: number) => { + if (isLoadingTools) { + return + } + setIsLoadingTools(true) + void message.loading({ content: '加载工具列表中', key: 'LOADING', duration: 0 }) + + void r_tool_store_get_by_username(username!, { currentPage: page }) + .then((res) => { + const response = res.data + + switch (response.code) { + case DATABASE_SELECT_SUCCESS: + setCurrentPage(response.data!.current) + if ( + response.data!.current === response.data!.pages || + response.data!.total === 0 + ) { + setHasNextPage(false) + } else { + setHasNextPage(true) + } + if (response.data!.current === 1) { + setToolData(response.data!.records) + } else { + setToolData([...toolData, ...response.data!.records]) + } + break + default: + void message.error('加载失败,请稍后重试') + } + }) + .finally(() => { + setIsLoadingTools(false) + message.destroy('LOADING') + }) + } + + useEffect(() => { + getProfile() + }, []) + + return ( + <> + + + + + + + } + size={144} + style={{ + background: COLOR_BACKGROUND, + cursor: 'pointer' + }} + className={'avatar'} + /> + + + + {userWithInfoVo?.userInfo.nickname} + + + {userWithInfoVo?.username && + new URL(`/store/${userWithInfoVo.username}`, location.href) + .href} + + + + + + {!toolData.length && ( + 该开发者暂未发布任何工具 + )} + {toolData?.map((value) => ( + + } + toolName={value.name} + toolId={value.toolId} + toolDesc={value.description} + url={`/view/${value.author.username}/${value.toolId}`} + authorUsername={value.author.username} + ver={value.ver} + /> + ))} + {hasNextPage && } + + + + + > + ) +} + +export default User diff --git a/src/pages/User/index.tsx b/src/pages/User/index.tsx index cf8147a..7af2d92 100644 --- a/src/pages/User/index.tsx +++ b/src/pages/User/index.tsx @@ -266,14 +266,14 @@ const User = () => { href={ userWithPowerInfoVo?.username && new URL( - `/view/${userWithPowerInfoVo.username}`, + `/store/${userWithPowerInfoVo.username}`, location.href ).href } > {userWithPowerInfoVo?.username && new URL( - `/view/${userWithPowerInfoVo.username}`, + `/store/${userWithPowerInfoVo.username}`, location.href ).href} diff --git a/src/router/tools.tsx b/src/router/tools.tsx index 5eb2764..12ef973 100644 --- a/src/router/tools.tsx +++ b/src/router/tools.tsx @@ -19,6 +19,13 @@ export const tools: RouteJsonObject[] = [ icon: lazy(() => import('~icons/oxygen/store')), menu: true }, + { + path: 'store/:username', + absolutePath: '/store', + id: 'tools-view-user', + component: lazy(() => import('@/pages/Tools/User')), + name: '开发者' + }, { path: 'create', absolutePath: '/create', diff --git a/src/services/system.tsx b/src/services/system.tsx index d5dd6af..0047416 100644 --- a/src/services/system.tsx +++ b/src/services/system.tsx @@ -26,6 +26,9 @@ import request from '@/services/index' export const r_sys_user_info_get = () => request.get(URL_SYS_USER_INFO) +export const r_sys_user_info_get_basic = (username: string) => + request.get(`${URL_SYS_USER_INFO}/${username}`) + export const r_sys_user_info_update = (param: UserInfoUpdateParam) => request.patch(URL_SYS_USER_INFO, param) diff --git a/src/services/tool.tsx b/src/services/tool.tsx index 601ec56..1bb8f18 100644 --- a/src/services/tool.tsx +++ b/src/services/tool.tsx @@ -33,3 +33,6 @@ export const r_tool_delete = (id: string) => request.delete(`${URL_TOOL}/${id}`) export const r_tool_store_get = (param: ToolStoreGetParam) => request.get>(URL_TOOL_STORE, param) + +export const r_tool_store_get_by_username = (username: string, param: ToolStoreGetParam) => + request.get>(`${URL_TOOL_STORE}/${username}`, param)