Refactor:11; Fix:6; Chore:1; Feat:7; Build:1 #49

Merged
FatttSnake merged 26 commits from FatttSnake into dev 2024-05-06 09:38:28 +08:00
83 changed files with 4023 additions and 3002 deletions

View File

@@ -34,6 +34,7 @@ module.exports = {
'warn',
{ allowConstantExport: true }
],
'@typescript-eslint/no-non-null-assertion': 'off'
'@typescript-eslint/no-non-null-assertion': 'off',
'react-hooks/exhaustive-deps': 'off',
}
}

3406
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,30 +15,33 @@
"preview": "vite preview"
},
"dependencies": {
"@ant-design/icons": "^5.2.6",
"@marsidev/react-turnstile": "^0.5.3",
"@ant-design/icons": "^5.3.6",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@marsidev/react-turnstile": "^0.6.0",
"@monaco-editor/react": "^4.6.0",
"@typescript/ata": "^0.9.4",
"antd": "^5.13.2",
"axios": "^1.6.5",
"antd": "^5.16.5",
"axios": "^1.6.8",
"custom-protocol-check": "^1.4.0",
"dayjs": "^1.11.10",
"echarts": "^5.4.3",
"esbuild-wasm": "^0.19.12",
"dayjs": "^1.11.11",
"echarts": "^5.5.0",
"esbuild-wasm": "^0.20.2",
"fast-deep-equal": "^3.1.3",
"fflate": "^0.8.1",
"fflate": "^0.8.2",
"jwt-decode": "^4.0.0",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
"match-sorter": "^6.3.3",
"match-sorter": "^6.3.4",
"moment": "^2.30.1",
"monaco-editor": "^0.45.0",
"monaco-editor": "^0.48.0",
"monaco-jsx-syntax-highlight": "^1.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-draggable": "^4.4.6",
"react-router": "^6.21.3",
"react-router-dom": "^6.21.3",
"react-router": "^6.23.0",
"react-router-dom": "^6.23.0",
"size-sensor": "^1.0.2",
"vanilla-tilt": "^1.8.1"
},
@@ -46,28 +49,28 @@
"@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0",
"@types/jsdom": "^21.1.6",
"@types/lodash": "^4.14.202",
"@types/node": "^20.11.5",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@typescript-eslint/eslint-plugin": "^6.19.1",
"@typescript-eslint/parser": "^6.19.1",
"@types/lodash": "^4.17.0",
"@types/node": "^20.12.7",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.8.0",
"@typescript-eslint/parser": "^7.8.0",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.56.0",
"eslint": "^8.57.0",
"eslint-config-love": "^47.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard-with-typescript": "^43.0.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.6",
"jsdom": "^24.0.0",
"prettier": "^3.2.4",
"sass": "^1.70.0",
"prettier": "^3.2.5",
"sass": "^1.76.0",
"stylelint-config-prettier": "^9.0.5",
"typescript": "^5.3.3",
"unplugin-auto-import": "^0.17.3",
"unplugin-icons": "^0.18.2",
"vite": "^5.0.12"
"typescript": "^5.4.5",
"unplugin-auto-import": "^0.17.5",
"unplugin-icons": "^0.19.0",
"vite": "^5.2.10"
}
}

View File

@@ -68,7 +68,6 @@
}
.vertical-scrollbar {
padding: 12px 2px;
height: 100%;
left: 100%;
top: 0;
@@ -80,7 +79,6 @@
}
.horizontal-scrollbar {
padding: 4px 12px;
width: 100%;
left: 0;
top: 100%;

View File

@@ -27,6 +27,7 @@
font-size: constants.$SIZE_ICON_SM;
border-radius: 8px;
cursor: pointer;
span {
transform: rotateZ(180deg);
transition: all .3s;
@@ -55,14 +56,16 @@
.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;
font-size: 1rem;
> .menu-bt {
border-radius: 8px;
@@ -78,6 +81,10 @@
height: 40px;
font-size: constants.$SIZE_ICON_SM;
cursor: pointer;
img {
width: 100%;
}
}
a {
@@ -86,15 +93,24 @@
height: 100%;
width: 100%;
transition: all 0.2s;
background-color: constants.$origin-color;
.text {
flex: 1;
padding-left: 8px;
width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
&.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);
}
}
}
}
@@ -102,7 +118,7 @@
.submenu {
visibility: hidden;
position: fixed;
padding-left: 20px;
padding-left: 10px;
z-index: 10000;
animation: 0.1s ease forwards;
@include mixins.unique-keyframes {
@@ -123,11 +139,13 @@
padding: 10px 10px;
background-color: constants.$origin-color;
border-radius: 8px;
box-shadow: 2px 2px 10px 0 rgba(0,0,0,0.1);
.item {
border-radius: 8px;
white-space: nowrap;
overflow: hidden;
padding: 0;
a {
display: block;
@@ -136,11 +154,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 +167,7 @@
&:hover {
> .menu-bt {
a {
a:not(.active) {
background-color: constants.$background-color;
}
}
@@ -171,6 +189,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;
}
}
}
}
}
@@ -251,13 +285,14 @@
transition: all .3s;
}
}
.text {
display: none;
}
}
.menu-bt {
.text {
.text, .extend {
display: none;
}
}
@@ -272,6 +307,7 @@
.footer {
position: relative;
.text {
display: none;
}
@@ -291,6 +327,7 @@
.icon-exit {
padding: 4px 8px;
&:hover {
border-radius: 8px;
background-color: constants.$background-color;

View File

@@ -0,0 +1,28 @@
@use '@/assets/css/constants' as constants;
[data-component=component-url-card] {
cursor: pointer;
.url-card {
width: 100%;
height: 100%;
margin-top: 80px;
text-align: center;
gap: 42px;
> * {
flex: 0 0 auto;
display: block;
}
.icon {
color: constants.$production-color;
font-size: constants.$SIZE_ICON_XL;
}
.text {
font-weight: bolder;
font-size: 2em;
}
}
}

View File

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

View File

@@ -0,0 +1,30 @@
@use "@/assets/css/constants" as constants;
[data-component=component-drop-mask] {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
padding: {
left: 10px;
right: 10px;
bottom: 10px;
};
background-color: constants.$origin-color;
.drop-mask-border {
display: flex;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
border: {
width: 2px;
color: constants.$font-secondary-color;
style: dashed;
radius: 8px;
};
font-size: 1.8em;
}
}

View File

@@ -0,0 +1,33 @@
@use '@/assets/css/constants' as constants;
[data-component=component-setting-card] {
.settings-card {
padding: 20px;
gap: 20px;
color: constants.$main-color;
> .head {
align-items: center;
gap: 5px;
.icon {
font-size: constants.$SIZE_ICON_MD;
flex: 0 0 auto;
}
.title {
display: flex;
font-size: 1.2em;
}
:nth-child(n+3) {
flex: 0 0 auto;
color: constants.$font-main-color;
}
.bt-save {
color: constants.$main-color;
}
}
}
}

View File

@@ -0,0 +1,78 @@
@use '@/assets/css/constants' as constants;
[data-component=component-statistics-card] {
.statistics-card {
padding: 20px;
gap: 20px;
> .head {
align-items: center;
gap: 5px;
color: constants.$main-color;
.icon {
font-size: constants.$SIZE_ICON_MD;
flex: 0 0 auto;
}
.title {
display: flex;
font-size: 1.2em;
}
:nth-child(n+3) {
flex: 0 0 auto;
color: constants.$font-main-color;
}
}
.card-content {
font-size: 1.1em;
padding: 0 10px;
gap: 10px;
.key, .value-percent {
flex: 0 0 auto;
color: constants.$font-main-color;
}
.value {
color: constants.$font-secondary-color;
overflow: hidden;
> * {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.value-chart {
justify-content: space-around;
width: 0;
> div {
max-height: 12px;
height: 12px;
> * {
transform: translateY(1px);
}
}
}
.value-percent {
text-align: right;
}
.big-chart {
width: 0;
height: 400px;
}
> * {
gap: 5px;
}
}
}
}

View File

@@ -0,0 +1,27 @@
@use '@/assets/css/constants' as constants;
[data-component=component-load-more-card] {
cursor: pointer;
.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;
}
}
}

View File

@@ -0,0 +1,51 @@
@use '@/assets/css/constants' as constants;
[data-component=component-repository-card] {
height: 100%;
.repository-card {
width: 100%;
height: 100%;
text-align: center;
align-items: center;
> * {
display: block;
flex: 0 0 auto;
}
.header {
display: flex;
width: 100%;
align-items: center;
padding: 10px;
.version-select {
width: 9em;
margin-right: auto;
}
>:not(.version-select) {
font-size: 1.6em;
}
}
.icon {
display: flex;
padding-top: 10px;
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;
}
}
}

View File

@@ -0,0 +1,110 @@
@use '@/assets/css/constants' as constants;
[data-component=component-store-card] {
height: 100%;
cursor: pointer;
.store-card {
width: 100%;
height: 100%;
text-align: center;
align-items: center;
> * {
display: block;
flex: 0 0 auto;
}
.header {
display: flex;
width: 100%;
padding: 10px;
justify-content: space-between;
.version {
width: 0;
transition: all 0.2s;
}
.operation {
display: flex;
font-size: 1.6em;
gap: 4px;
opacity: 0;
transition: all 0.2s;
> *:hover {
color: constants.$font-secondary-color;
}
}
}
.icon {
display: flex;
padding-top: 10px;
padding-bottom: 20px;
color: constants.$production-color;
font-size: constants.$SIZE_ICON_XL;
justify-content: center;
img {
width: constants.$SIZE_ICON_XL;
}
}
.info {
padding-top: 20px;
.tool-name {
font-weight: bolder;
font-size: 1.6em;
}
.tool-desc {
margin: {
top: 10px;
left: auto;
right: auto;
};
color: constants.$font-secondary-color;
overflow: hidden;
text-overflow: ellipsis;
max-height: 40px;
width: 80%;
}
}
.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;
}
}
}
:hover {
.header {
.version {
opacity: 0;
}
.operation {
opacity: 1;
}
}
}
}

View File

@@ -9,33 +9,9 @@
> .card-box {
width: 200px;
height: 360px;
height: 320px;
flex: 0 0 auto;
overflow: hidden !important;
cursor: pointer;
.common-card {
width: 100%;
height: 100%;
margin-top: 100px;
text-align: center;
gap: 42px;
> * {
flex: 0 0 auto;
display: block;
}
.icon {
color: constants.$production-color;
font-size: constants.$SIZE_ICON_XL;
}
.text {
font-weight: bolder;
font-size: 2em;
}
}
}
}
}

View File

@@ -11,36 +11,6 @@
> * {
flex: 0 0 auto;
}
.settings-card {
padding: 20px;
gap: 20px;
color: constants.$main-color;
> .head {
align-items: center;
gap: 5px;
.icon {
font-size: constants.$SIZE_ICON_MD;
flex: 0 0 auto;
}
.title {
display: flex;
font-size: 1.2em;
}
:nth-child(n+3) {
flex: 0 0 auto;
color: constants.$font-main-color;
}
.bt-save {
color: constants.$main-color;
}
}
}
}
}
}

View File

@@ -10,81 +10,6 @@
> .card-box {
width: 48%;
flex: 0 0 auto;
.common-card {
padding: 20px;
gap: 20px;
> .head {
align-items: center;
gap: 5px;
color: constants.$main-color;
.icon {
font-size: constants.$SIZE_ICON_MD;
flex: 0 0 auto;
}
.title {
display: flex;
font-size: 1.2em;
}
:nth-child(n+3) {
flex: 0 0 auto;
color: constants.$font-main-color;
}
}
.card-content {
font-size: 1.1em;
padding: 0 10px;
gap: 10px;
.key, .value-percent {
flex: 0 0 auto;
color: constants.$font-main-color;
}
.value {
color: constants.$font-secondary-color;
overflow: hidden;
> * {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.value-chart {
justify-content: space-around;
width: 0;
> div {
max-height: 12px;
height: 12px;
> * {
transform: translateY(1px);
}
}
}
.value-percent {
text-align: right;
}
.big-chart {
width: 0;
height: 400px;
}
> * {
gap: 5px;
}
}
}
}
}
}

View File

@@ -13,6 +13,7 @@
}
>*:first-child {
width: 0;
height: fit-content;
}

View File

@@ -0,0 +1,43 @@
@use '@/assets/css/constants' as constants;
[data-component=system-tools-template] {
.root-content {
padding: 20px;
gap: 10px;
height: 100%;
width: 100%;
.has-edited::after {
content: '*';
color: constants.$font-secondary-color;
}
>*:first-child {
width: 0;
height: fit-content;
}
> *:nth-child(2) {
position: sticky;
top: 20px;
height: calc(100vh - 40px);
}
.close-editor-btn {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 10px;
right: 10px;
background-color: constants.$font-secondary-color;
width: 32px;
height: 32px;
border-radius: 50%;
color: white;
opacity: 0.6;
box-shadow: 2px 2px 10px 0 rgba(0,0,0,0.2);
cursor: pointer;
}
}
}

View File

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

View File

@@ -5,57 +5,16 @@
.root-content {
padding: 20px;
gap: 20px;
.own-content {
gap: 20px;
flex-wrap: wrap;
justify-content: flex-start;
> .card-box {
> .card-box, > div {
width: 180px;
height: 290px;
flex: 0 0 auto;
.common-card {
width: 100%;
height: 100%;
text-align: center;
align-items: center;
> * {
display: block;
flex: 0 0 auto;
}
.version-select {
position: absolute;
top: 10px;
left: 10px;
width: 9em;
}
.upgrade-bt {
position: absolute;
top: 10px;
right: 10px;
font-size: 1.8em;
}
.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;
}
}
}
@@ -112,4 +71,40 @@
}
}
}
.favorite-divider {
display: flex;
flex-direction: row;
align-items: center;
gap: 20px;
margin-top: 20px;
:first-child, :last-child {
height: 0;
border: {
width: 1px;
color: constants.$divide-color;
style: dashed;
};
}
.divider-text {
flex: 0 0 auto;
font-size: 1.2em;
color: constants.$font-secondary-color;
}
}
.star-content {
gap: 20px;
flex-wrap: wrap;
justify-content: flex-start;
> .card-box, > div {
width: 180px;
height: 290px;
flex: 0 0 auto;
}
}
}
}

View File

@@ -26,113 +26,10 @@
flex-wrap: wrap;
justify-content: center;
> .card-box {
> div {
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 {
display: flex;
position: absolute;
top: 10px;
right: 12px;
font-size: 1.6em;
gap: 4px;
> *: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 {

View File

@@ -68,113 +68,10 @@
flex-wrap: wrap;
justify-content: center;
> .card-box {
> div {
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 {
display: flex;
position: absolute;
top: 10px;
right: 12px;
font-size: 1.6em;
gap: 4px;
> *: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 {

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

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M927.929032 640.065509a31.994541 31.994541 0 0 0-31.994541 31.994541v127.978163c0 52.918971-43.064652 95.983623-95.983623 95.983623h-575.901736c-52.918971 0-95.983623-43.064652-95.983623-95.983623v-127.978163a31.994541 31.994541 0 0 0-63.989082 0v127.978163C64.076427 888.279157 135.872177 960.010918 224.049132 960.010918h575.901736c88.240944 0 159.972705-71.731761 159.972705-159.972705v-127.978163a31.994541 31.994541 0 0 0-31.994541-31.994541z" /><path d="M412.113043 777.386079c18.684812 18.684812 43.256619 28.027218 67.892416 28.027217s49.143615-9.342406 67.892416-28.027217l274.705128-274.705129A31.930552 31.930552 0 0 0 799.950868 448.098263h-159.140846a629.524587 629.524587 0 0 1 44.408423-202.909378L765.652721 44.0712a32.122519 32.122519 0 0 0-7.998636-35.449951 32.122519 32.122519 0 0 0-35.961864-5.055137L566.582687 81.120879A443.124392 443.124392 0 0 0 321.184558 448.098263H160.06005a31.994541 31.994541 0 0 0-22.652135 54.646676l274.705128 274.64114zM352.027295 512.023356a31.994541 31.994541 0 0 0 31.994541-31.994541A379.711212 379.711212 0 0 1 595.185806 138.391107l79.858375-39.929187-49.207604 122.923026A692.7458 692.7458 0 0 0 575.989082 480.092804a31.994541 31.994541 0 0 0 31.994541 31.994541h114.732424l-220.058453 220.058453a31.994541 31.994541 0 0 1-45.240281 0L237.294872 512.087345 352.027295 512.023356z" /></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

1
src/assets/svg/star.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1151 1024"><path d="M1056.069394 342.982943L763.905526 300.38821 633.321676 35.620955c-23.397106-47.194163-91.188723-47.794089-114.785804 0L387.952021 300.38821 95.788154 342.982943c-52.39352 7.59906-73.390924 72.191072-35.395623 109.186496l211.373859 205.974527-49.993817 290.964016c-8.998887 52.593496 46.394262 91.988624 92.788525 67.391666L575.928774 879.116638l261.367676 137.38301c46.394262 24.396983 101.787412-14.79817 92.788525-67.391666l-49.993818-290.964016 211.373859-205.974527c37.995301-36.995425 16.997898-101.587436-35.395622-109.186496zM777.103894 624.548121l47.394139 276.765772L575.928774 770.730042l-248.569259 130.583851 47.394139-276.765772-201.175121-195.975763 277.965624-40.395005 124.384617-251.968838 124.384617 251.968838 277.965623 40.395005-201.17512 195.975763z" /></svg>

After

Width:  |  Height:  |  Size: 856 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1151 1024"><path d="M518.535872 35.620955L387.952021 300.38821 95.788154 342.982943c-52.39352 7.59906-73.390924 72.191072-35.395623 109.186496l211.373859 205.974527-49.993817 290.964016c-8.998887 52.593496 46.394262 91.988624 92.788525 67.391666L575.928774 879.116638l261.367676 137.38301c46.394262 24.396983 101.787412-14.79817 92.788525-67.391666l-49.993818-290.964016 211.373859-205.974527c37.995301-36.995425 16.997898-101.587436-35.395622-109.186496L763.905526 300.38821 633.321676 35.620955c-23.397106-47.194163-91.188723-47.794089-114.785804 0z" /></svg>

After

Width:  |  Height:  |  Size: 615 B

View File

@@ -2,6 +2,6 @@ import { loader } from '@monaco-editor/react'
loader.config({
paths: {
vs: 'https://unpkg.com/monaco-editor@0.45.0/min/vs'
vs: 'https://unpkg.com/monaco-editor@0.48.0/min/vs'
}
})

View File

@@ -166,7 +166,13 @@ const FileSelector = ({
<>
<div data-component={'playground-file-selector'} className={'tab'}>
<div className={'multiple'}>
<HideScrollbar ref={hideScrollbarRef}>
<HideScrollbar
ref={hideScrollbarRef}
autoHideWaitingTime={800}
scrollbarWidth={1}
scrollbarAsidePadding={0}
scrollbarEdgePadding={0}
>
<FlexBox direction={'horizontal'} className={'tab-content'}>
{tabs.map((item, index) => (
<Item

View File

@@ -16,7 +16,7 @@ class Compiler {
void esbuild
.initialize({
worker: true,
wasmURL: 'https://esm.sh/esbuild-wasm@0.19.12/esbuild.wasm'
wasmURL: 'https://esm.sh/esbuild-wasm@0.20.2/esbuild.wasm'
})
.finally(() => {
this.init = true

View File

@@ -21,6 +21,8 @@ interface HideScrollbarProps
minHeight?: string | number
scrollbarWidth?: string | number
autoHideWaitingTime?: number
scrollbarAsidePadding?: number
scrollbarEdgePadding?: number
}
export interface HideScrollbarElement {
@@ -74,6 +76,8 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
children,
style,
className,
scrollbarAsidePadding = 12,
scrollbarEdgePadding = 4,
...props
},
ref
@@ -578,7 +582,8 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
? maskRef.current?.clientLeft +
maskRef.current?.clientWidth -
1
: undefined
: undefined,
padding: `${scrollbarAsidePadding}px ${scrollbarEdgePadding}px`
}}
>
<div className={'box'} style={{ width: scrollbarWidth }}>
@@ -620,7 +625,8 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
? maskRef.current?.clientTop +
maskRef.current?.clientHeight -
1
: undefined
: undefined,
padding: `${scrollbarEdgePadding}px ${scrollbarAsidePadding}px`
}}
>
<div className={'box'} style={{ height: scrollbarWidth }}>

View File

@@ -2,6 +2,7 @@ import Icon from '@ant-design/icons'
import { COLOR_ERROR } from '@/constants/common.constants'
import { getRedirectUrl } from '@/util/route'
import { getAvatar, getLoginStatus, getNickname, removeToken } from '@/util/auth'
import { navigateToLogin, navigateToUser } from '@/util/navigation'
import { r_auth_logout } from '@/services/auth'
const Footer = () => {
@@ -15,9 +16,9 @@ const Footer = () => {
const handleClickAvatar = () => {
if (getLoginStatus()) {
navigate('/user')
navigateToUser(navigate)
} else {
navigate(getRedirectUrl('/login', `${lastMatch.pathname}${location.search}`))
navigateToLogin(navigate, undefined, `${lastMatch.pathname}${location.search}`)
}
}

View File

@@ -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
}
@@ -41,10 +42,21 @@ const Item = (props: ItemProps) => {
isPending ? 'pending' : isActive ? 'active' : ''
}
>
{props.icon && (
<div className={'icon-box'}>
{props.icon && <Icon className={'icon'} component={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>
)}
<span className={'text'}>{props.text}</span>
<div className={'extend'}>{props.extend}</div>
</NavLink>
</div>
{props.children && (

View File

@@ -0,0 +1,57 @@
import { DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react'
import Icon from '@ant-design/icons'
import VanillaTilt, { TiltOptions } from 'vanilla-tilt'
import '@/assets/css/components/common/url-card.scss'
import Card from '@/components/common/Card'
import FlexBox from '@/components/common/FlexBox'
interface UrlCardProps extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
icon: IconComponent
description?: ReactNode
options?: TiltOptions
url?: string
}
const UrlCard = ({
style,
icon,
description,
options = {
reverse: true,
max: 8,
glare: true,
scale: 1.03
},
url,
children,
...props
}: UrlCardProps) => {
const navigate = useNavigate()
const cardRef = useRef<HTMLDivElement>(null)
useEffect(() => {
cardRef.current && VanillaTilt.init(cardRef.current, options)
}, [options])
const handleCardOnClick = () => {
url && navigate(url)
}
return (
<Card
data-component={'component-url-card'}
style={{ overflow: 'visible', ...style }}
{...props}
ref={cardRef}
onClick={handleCardOnClick}
>
<FlexBox className={'url-card'}>
<Icon component={icon} className={'icon'} />
<div className={'text'}>{children}</div>
<div className={'description'}>{description}</div>
</FlexBox>
</Card>
)
}
export default UrlCard

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,14 @@
import '@/assets/css/components/dnd/drop-mask.scss'
import Icon from '@ant-design/icons'
const DropMask = () => {
return (
<div data-component={'component-drop-mask'}>
<div className={'drop-mask-border'}>
<Icon component={IconOxygenReceive} />
</div>
</div>
)
}
export default DropMask

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

@@ -0,0 +1,48 @@
import { PropsWithChildren, ReactNode } from 'react'
import Icon from '@ant-design/icons'
import '@/assets/css/components/system/setting-card.scss'
import Card from '@/components/common/Card'
import FlexBox from '@/components/common/FlexBox'
import Permission from '@/components/common/Permission'
import LoadingMask from '@/components/common/LoadingMask'
interface SettingsCardProps extends PropsWithChildren {
icon: IconComponent
title: string
loading?: boolean
modifyOperationCode?: string[]
expand?: ReactNode
onReset?: () => void
onSave?: () => void
}
export const SettingsCard = (props: SettingsCardProps) => {
return (
<Card data-component={'component-setting-card'}>
<FlexBox className={'settings-card'}>
<FlexBox direction={'horizontal'} className={'head'}>
<Icon component={props.icon} className={'icon'} />
<div className={'title'}>{props.title}</div>
{!props.loading && (
<Permission operationCode={props.modifyOperationCode}>
{props.expand}
<AntdButton onClick={props.onReset} title={'重置'}>
<Icon component={IconOxygenBack} />
</AntdButton>
<AntdButton className={'bt-save'} onClick={props.onSave} title={'保存'}>
<Icon component={IconOxygenSave} />
</AntdButton>
</Permission>
)}
</FlexBox>
<LoadingMask
maskContent={<AntdSkeleton active paragraph={{ rows: 6 }} />}
hidden={!props.loading}
>
{props.children}
</LoadingMask>
</FlexBox>
</Card>
)
}
export default SettingsCard

View File

@@ -0,0 +1,35 @@
import { PropsWithChildren, ReactNode } from 'react'
import Icon from '@ant-design/icons'
import '@/assets/css/components/system/statistics-card.scss'
import Card from '@/components/common/Card'
import FlexBox from '@/components/common/FlexBox'
import LoadingMask from '@/components/common/LoadingMask'
interface StatisticsCardProps extends PropsWithChildren {
icon: IconComponent
title: ReactNode
loading?: boolean
expand?: ReactNode
}
export const StatisticsCard = (props: StatisticsCardProps) => {
return (
<Card data-component={'component-statistics-card'} style={{ overflow: 'visible' }}>
<FlexBox className={'statistics-card'}>
<FlexBox direction={'horizontal'} className={'head'}>
<Icon component={props.icon} className={'icon'} />
<div className={'title'}>{props.title}</div>
{props.expand}
</FlexBox>
<LoadingMask
hidden={!props.loading}
maskContent={<AntdSkeleton active paragraph={{ rows: 6 }} />}
>
{props.children}
</LoadingMask>
</FlexBox>
</Card>
)
}
export default StatisticsCard

View File

@@ -0,0 +1,42 @@
import VanillaTilt from 'vanilla-tilt'
import Icon from '@ant-design/icons'
import '@/assets/css/components/tools/load-more-card.scss'
import FlexBox from '@/components/common/FlexBox'
import Card from '@/components/common/Card'
interface LoadMoreCardProps {
onClick: () => void
}
const LoadMoreCard = ({ onClick }: LoadMoreCardProps) => {
const cardRef = useRef<HTMLDivElement>(null)
useEffect(() => {
cardRef.current &&
VanillaTilt.init(cardRef.current, {
reverse: true,
max: 8,
glare: true,
['max-glare']: 0.3,
scale: 1.03
})
}, [])
return (
<Card
data-component={'component-load-more-card'}
style={{ overflow: 'visible' }}
ref={cardRef}
onClick={onClick}
>
<FlexBox className={'load-more-card'}>
<div className={'icon'}>
<Icon component={IconOxygenMore} />{' '}
</div>
<div className={'text'}></div>
</FlexBox>
</Card>
)
}
export default LoadMoreCard

View File

@@ -0,0 +1,115 @@
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'
interface RepositoryCardProps
extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
icon: string
toolName: string
toolId: string
ver: string
platform: Platform
options?: TiltOptions
onOpen?: () => void
onEdit?: () => void
onSource?: () => void
onPublish?: () => void
onCancelReview?: () => void
onDelete?: () => void
}
const RepositoryCard = ({
style,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
ref,
icon,
toolName,
toolId,
ver,
platform,
options = {
reverse: true,
max: 8,
glare: true,
['max-glare']: 0.3,
scale: 1.03
},
onOpen,
onEdit,
onSource,
onPublish,
onCancelReview,
onDelete,
children,
...props
}: RepositoryCardProps) => {
const cardRef = useRef<HTMLDivElement>(null)
useEffect(() => {
cardRef.current && VanillaTilt.init(cardRef.current, options)
}, [options])
return (
<Draggable
id={`!:${toolId}:${ver}:${platform}`}
data={{ icon, toolName, toolId, authorUsername: '!', ver, platform }}
>
<Card
data-component={'component-repository-card'}
style={{ overflow: 'visible', ...style }}
ref={cardRef}
{...props}
>
<FlexBox className={'repository-card'}>
<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={'tool-name'}>{toolName}</div>
<div className={'tool-id'}>{`ID: ${toolId}`}</div>
</div>
<div className={'operation'}>
{onOpen && (
<AntdButton onClick={onOpen} size={'small'} type={'primary'}>
</AntdButton>
)}
{onEdit && onPublish && (
<div className={'edit'}>
<AntdButton.Group size={'small'}>
<AntdButton onClick={onEdit}></AntdButton>
<AntdButton onClick={onPublish}></AntdButton>
</AntdButton.Group>
</div>
)}
{onSource && (
<AntdButton size={'small'} onClick={onSource}>
</AntdButton>
)}
{onCancelReview && (
<AntdButton size={'small'} onClick={onCancelReview}>
</AntdButton>
)}
{onDelete && (
<AntdButton size={'small'} danger onClick={onDelete}>
</AntdButton>
)}
</div>
</FlexBox>
</Card>
</Draggable>
)
}
export default RepositoryCard

View File

@@ -0,0 +1,312 @@
import { DetailedHTMLProps, HTMLAttributes, MouseEvent } from 'react'
import VanillaTilt, { TiltOptions } from 'vanilla-tilt'
import protocolCheck from 'custom-protocol-check'
import Icon from '@ant-design/icons'
import '@/assets/css/components/tools/store-card.scss'
import { COLOR_BACKGROUND, COLOR_MAIN, COLOR_PRODUCTION } from '@/constants/common.constants'
import { checkDesktop, omitText } from '@/util/common'
import { getLoginStatus, getUserId } from '@/util/auth'
import {
navigateToLogin,
navigateToSource,
navigateToStore,
navigateToView
} from '@/util/navigation'
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 DragHandle from '@/components/dnd/DragHandle'
import Draggable from '@/components/dnd/Draggable'
interface StoreCardProps extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
icon: string
toolName: string
toolId: string
toolDesc: string
options?: TiltOptions
author: UserWithInfoVo
showAuthor?: boolean
ver: string
platform: Platform
supportPlatform: Platform[]
favorite: boolean
}
const StoreCard = ({
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
},
author,
showAuthor = true,
ver,
platform,
supportPlatform,
favorite,
...props
}: StoreCardProps) => {
const navigate = useNavigate()
const [modal, contextHolder] = AntdModal.useModal()
const cardRef = useRef<HTMLDivElement>(null)
const [favorite_, setFavorite_] = useState<boolean>(favorite)
const [userId, setUserId] = useState('')
useEffect(() => {
cardRef.current && VanillaTilt.init(cardRef.current, options)
if (getLoginStatus()) {
void getUserId().then((value) => setUserId(value))
}
}, [options])
const handleCardOnClick = () => {
if (!checkDesktop() && platform === 'DESKTOP') {
void message.warning('此应用需要桌面端环境,请在桌面端打开')
return
}
if (platform === 'ANDROID') {
void modal.confirm({
icon: <Icon style={{ color: COLOR_MAIN }} component={IconOxygenInfo} />,
title: 'Android 端',
centered: true,
maskClosable: true,
content: (
<FlexBox className={'android-qrcode'}>
<AntdQRCode
value={`oxygen://openurl/view/${author.username}/${toolId}`}
size={300}
/>
<AntdTag className={'tag'}>使</AntdTag>
</FlexBox>
),
okText: '确定',
cancelText: '模拟器',
onCancel() {
navigateToView(navigate, author.username, toolId, platform)
}
})
return
}
navigateToView(navigate, author.username, toolId, platform)
}
const handleOnClickAuthor = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
navigateToStore(navigate, author.username)
}
const handleOnSourceBtnClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
navigateToSource(navigate, author.username, toolId, platform)
}
const handleOnStarBtnClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
if (!getLoginStatus()) {
navigateToLogin(navigate, undefined, `${location.pathname}${location.search}`)
return
}
if (favorite_) {
void r_tool_remove_favorite({
authorId: author.id,
toolId: toolId,
platform: platform
}).then((res) => {
const response = res.data
if (response.success) {
setFavorite_(false)
} else {
void message.error('取消收藏失败,请稍后重试')
}
})
} else {
void r_tool_add_favorite({
authorId: author.id,
toolId: toolId,
platform: platform
}).then((res) => {
const response = res.data
if (response.success) {
setFavorite_(true)
} else {
void message.error('收藏失败,请稍后重试')
}
})
}
}
const handleOnAndroidBtnClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
void modal.confirm({
icon: <Icon style={{ color: COLOR_MAIN }} component={IconOxygenInfo} />,
title: 'Android 端',
centered: true,
maskClosable: true,
content: (
<FlexBox className={'android-qrcode'}>
<AntdQRCode
value={`oxygen://openurl/view/${author.username}/${toolId}`}
size={300}
/>
<AntdTag className={'tag'}>使</AntdTag>
</FlexBox>
),
okText: '确定',
cancelText: '模拟器',
onCancel() {
navigateToView(navigate, author.username, toolId, 'ANDROID')
}
})
}
const handleOnDesktopBtnClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
if (!checkDesktop()) {
void message.loading({ content: '启动桌面端中……', key: 'LOADING', duration: 0 })
protocolCheck(
`oxygen://openurl/view/${author.username}/${toolId}`,
() => {
void message.warning('打开失败,此应用需要桌面端环境,请安装桌面端后重试')
void message.destroy('LOADING')
},
() => {
void message.destroy('LOADING')
},
2000,
() => {
void message.warning('打开失败,此应用需要桌面端环境,请安装桌面端后重试')
void message.destroy('LOADING')
}
)
return
}
navigateToView(navigate, author.username, toolId, 'DESKTOP')
}
const handleOnWebBtnClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
navigateToView(navigate, author.username, toolId, 'WEB')
}
return (
<>
<Draggable
id={`${author.username}:${toolId}:${ver}:${platform}`}
data={{
icon,
toolName,
toolId,
authorUsername: author.username,
ver: '',
platform: platform
}}
>
<Card
data-component={'component-store-card'}
style={{ overflow: 'visible', ...style }}
ref={cardRef}
{...props}
onClick={handleCardOnClick}
>
<FlexBox className={'store-card'}>
<div className={'header'}>
<div className={'version'}>
<AntdTag>
{platform.slice(0, 1)}-{ver}
</AntdTag>
</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={'tool-name'}>{toolName}</div>
<div className={'tool-id'}>{`ID: ${toolId}`}</div>
{toolDesc && (
<div
className={'tool-desc'}
title={toolDesc}
>{`简介:${omitText(toolDesc, 18)}`}</div>
)}
</div>
{showAuthor && (
<div className={'author'} onClick={handleOnClickAuthor}>
<div className={'avatar'}>
<AntdAvatar
src={
<AntdImage
preview={false}
src={`data:image/png;base64,${author.userInfo.avatar}`}
alt={'Avatar'}
/>
}
style={{ background: COLOR_BACKGROUND }}
/>
</div>
<div className={'author-name'}>{author.userInfo.nickname}</div>
</div>
)}
</FlexBox>
</Card>
</Draggable>
{contextHolder}
</>
)
}
export default StoreCard

View File

@@ -1,6 +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_TOOL_MENU_ITEM_KEY = 'TOOL_MENU_ITEM'
export const COLOR_ORIGIN = 'white'
export const COLOR_PRODUCTION = '#4E47BB'
export const COLOR_MAIN = COLOR_PRODUCTION
@@ -77,6 +78,7 @@ export const DATABASE_DELETE_FILED = 30035
export const DATABASE_EXECUTE_ERROR = 30050
export const DATABASE_DUPLICATE_KEY = 30051
export const DATABASE_NO_RECORD_FOUND = 30052
export const DATABASE_RECORD_ALREADY_EXISTS = 30053
export const TOOL_SUBMIT_SUCCESS = 40010
export const TOOL_CANCEL_SUCCESS = 40011

View File

@@ -38,6 +38,7 @@ export const URL_TOOL_STORE = `${URL_TOOL}/store`
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_TOOL_FAVORITE = `${URL_TOOL_STORE}/favorite`
export const URL_API_V1 = '/api/v1'
export const URL_API_V1_AVATAR_RANDOM_BASE64 = `${URL_API_V1}/avatar/base64`

16
src/global.d.ts vendored
View File

@@ -604,6 +604,7 @@ interface ToolVo {
review: 'NONE' | 'PROCESSING' | 'PASS' | 'REJECT'
createTime: string
updateTime: string
favorite: boolean
}
interface ToolCreateParam {
@@ -647,3 +648,18 @@ interface ToolManagementPassParam {
interface ToolStoreGetParam extends PageParam {
searchValue?: string
}
interface ToolFavoriteAddRemoveParam {
authorId: string
toolId: string
platform: Platform
}
interface ToolMenuItem {
icon: string
toolName: string
toolId: string
authorUsername: string
ver: string
platform: Platform
}

View File

@@ -8,6 +8,7 @@ import {
PERMISSION_USER_NOT_FOUND,
SYSTEM_INVALID_CAPTCHA_CODE
} from '@/constants/common.constants'
import { navigateToLogin } from '@/util/navigation'
import { r_auth_forget, r_auth_retrieve } from '@/services/auth'
import FitCenter from '@/components/common/FitCenter'
import FlexBox from '@/components/common/FlexBox'
@@ -272,7 +273,15 @@ const Forget = () => {
<div className={'footer'}>
<a onClick={() => navigate(`/login`, { replace: true })}></a>
<a
onClick={() =>
navigateToLogin(navigate, location.search, undefined, {
replace: true
})
}
>
</a>
</div>
</div>
</FlexBox>

View File

@@ -12,6 +12,12 @@ import {
} from '@/constants/common.constants'
import { getUserInfo, setToken } from '@/util/auth'
import { utcToLocalTime } from '@/util/datetime'
import {
navigateToForget,
navigateToRedirect,
navigateToRegister,
navigateToRoot
} from '@/util/navigation'
import { r_auth_login } from '@/services/auth'
import { AppContext } from '@/App'
import FitCenter from '@/components/common/FitCenter'
@@ -78,11 +84,7 @@ const SignIn = () => {
setTimeout(() => {
void getUserInfo().then((user) => {
refreshRouter()
if (searchParams.has('redirect')) {
navigate(searchParams.get('redirect') ?? '/')
} else {
navigate('/')
}
navigateToRedirect(navigate, searchParams, '/repository')
notification.success({
message: '欢迎回来',
@@ -110,8 +112,8 @@ const SignIn = () => {
case PERMISSION_NEED_TWO_FACTOR:
twoFactorForm.resetFields()
void modal.confirm({
title: '双因素验证',
centered: true,
title: '双因素验证',
footer: (_, { OkBtn, CancelBtn }) => (
<>
<OkBtn />
@@ -259,14 +261,14 @@ const SignIn = () => {
<FlexBox direction={'horizontal'} className={'addition'}>
<a
onClick={() => {
navigate('/')
navigateToRoot(navigate)
}}
>
</a>
<a
onClick={() => {
navigate(`/forget${location.search}`, { replace: true })
navigateToForget(navigate, location.search, { replace: true })
}}
>
@@ -287,7 +289,7 @@ const SignIn = () => {
<a
onClick={() =>
navigate(`/register${location.search}`, { replace: true })
navigateToRegister(navigate, location.search, { replace: true })
}
>

View File

@@ -8,6 +8,7 @@ import {
SYSTEM_MATCH_SENSITIVE_WORD
} from '@/constants/common.constants'
import { getLoginStatus, setToken } from '@/util/auth'
import { navigateToLogin } from '@/util/navigation'
import { r_auth_register, r_auth_resend } from '@/services/auth'
import FitCenter from '@/components/common/FitCenter'
import FlexBox from '@/components/common/FlexBox'
@@ -49,9 +50,7 @@ const SignUp = () => {
return
}
if (getLoginStatus()) {
navigate(`/login${location.search}`, {
replace: true
})
navigateToLogin(navigate, location.search, undefined, { replace: true })
}
}, [location.pathname])
@@ -245,7 +244,9 @@ const SignUp = () => {
<a
onClick={() =>
navigate(`/login${location.search}`, { replace: true })
navigateToLogin(navigate, location.search, undefined, {
replace: true
})
}
>

View File

@@ -7,7 +7,7 @@ import {
SYSTEM_MATCH_SENSITIVE_WORD
} from '@/constants/common.constants'
import { getLoginStatus, getUserInfo, requestUserInfo } from '@/util/auth'
import { getRedirectUrl } from '@/util/route'
import { navigateToLogin, navigateToRedirect, navigateToRepository } from '@/util/navigation'
import { r_auth_resend, r_auth_verify } from '@/services/auth'
import { r_api_avatar_random_base64 } from '@/services/api/avatar'
import { AppContext } from '@/App'
@@ -32,7 +32,7 @@ const Verify = () => {
}
if (!getLoginStatus()) {
navigate(getRedirectUrl('/login', `${location.pathname}${location.search}`), {
navigateToLogin(navigate, location.search, undefined, {
replace: true
})
return
@@ -81,7 +81,7 @@ const Verify = () => {
break
case PERMISSION_NO_VERIFICATION_REQUIRED:
void message.warning('账户已验证')
navigate('/')
navigateToRepository(navigate)
break
default:
void message.error('出错了,请稍后重试')
@@ -128,11 +128,7 @@ const Verify = () => {
setTimeout(() => {
void requestUserInfo().then(() => {
refreshRouter()
if (searchParams.has('redirect')) {
navigate(searchParams.get('redirect') ?? '/')
} else {
navigate('/')
}
navigateToRedirect(navigate, searchParams, '/repository')
})
}, 1500)
break

View File

@@ -1,6 +1,6 @@
import { hasPermission } from '@/util/auth'
import { r_sys_settings_base_get, r_sys_settings_base_update } from '@/services/system'
import { SettingsCard } from '@/pages/System/Settings'
import SettingsCard from '@/components/system/SettingCard'
const Base = () => {
const [baseForm] = AntdForm.useForm<BaseSettingsParam>()

View File

@@ -5,7 +5,7 @@ import {
r_sys_settings_mail_send,
r_sys_settings_mail_update
} from '@/services/system'
import { SettingsCard } from '@/pages/System/Settings'
import SettingsCard from '@/components/system/SettingCard'
const Mail = () => {
const [modal, contextHolder] = AntdModal.useModal()
@@ -16,9 +16,9 @@ const Mail = () => {
const handleOnTest = () => {
void modal.confirm({
title: '发送测试邮件',
centered: true,
maskClosable: true,
title: '发送测试邮件',
footer: (_, { OkBtn, CancelBtn }) => (
<>
<OkBtn />

View File

@@ -7,7 +7,7 @@ import {
r_sys_settings_sensitive_get,
r_sys_settings_sensitive_update
} from '@/services/system'
import { SettingsCard } from '@/pages/System/Settings'
import SettingsCard from '@/components/system/SettingCard'
const SensitiveWord = () => {
const [dataSource, setDataSource] = useState<SensitiveWordVo[]>()

View File

@@ -1,6 +1,6 @@
import { hasPermission } from '@/util/auth'
import { r_sys_settings_two_factor_get, r_sys_settings_two_factor_update } from '@/services/system'
import { SettingsCard } from '@/pages/System/Settings'
import SettingsCard from '@/components/system/SettingCard'
const TwoFactor = () => {
const [twoFactorForm] = AntdForm.useForm<TwoFactorSettingsParam>()

View File

@@ -1,55 +1,12 @@
import { PropsWithChildren, ReactNode } from 'react'
import Icon from '@ant-design/icons'
import '@/assets/css/pages/system/settings.scss'
import FitFullscreen from '@/components/common/FitFullscreen'
import HideScrollbar from '@/components/common/HideScrollbar'
import Card from '@/components/common/Card'
import FlexBox from '@/components/common/FlexBox'
import LoadingMask from '@/components/common/LoadingMask'
import Permission from '@/components/common/Permission'
import Base from '@/pages/System/Settings/Base'
import Mail from '@/pages/System/Settings/Mail'
import SensitiveWord from '@/pages/System/Settings/SensitiveWord'
import TwoFactor from '@/pages/System/Settings/TwoFactor.tsx'
interface SettingsCardProps extends PropsWithChildren {
icon: IconComponent
title: string
loading?: boolean
modifyOperationCode?: string[]
expand?: ReactNode
onReset?: () => void
onSave?: () => void
}
export const SettingsCard = (props: SettingsCardProps) => {
return (
<Card>
<FlexBox className={'settings-card'}>
<FlexBox direction={'horizontal'} className={'head'}>
<Icon component={props.icon} className={'icon'} />
<div className={'title'}>{props.title}</div>
{!props.loading && (
<Permission operationCode={props.modifyOperationCode}>
{props.expand}
<AntdButton onClick={props.onReset} title={'重置'}>
<Icon component={IconOxygenBack} />
</AntdButton>
<AntdButton className={'bt-save'} onClick={props.onSave} title={'保存'}>
<Icon component={IconOxygenSave} />
</AntdButton>
</Permission>
)}
</FlexBox>
<LoadingMask
maskContent={<AntdSkeleton active paragraph={{ rows: 6 }} />}
hidden={!props.loading}
>
{props.children}
</LoadingMask>
</FlexBox>
</Card>
)
}
import TwoFactor from '@/pages/System/Settings/TwoFactor'
const Settings = () => {
return (

View File

@@ -4,7 +4,7 @@ import { getTimesBetweenTwoTimes } from '@/util/datetime'
import { r_sys_statistics_active } from '@/services/system'
import FlexBox from '@/components/common/FlexBox'
import { getTooltipTimeFormatter, lineEChartsBaseOption } from '@/pages/System/Statistics/shared'
import { CommonCard } from '@/pages/System/Statistics'
import StatisticsCard from '@/components/system/StatisticsCard'
const ActiveInfo = () => {
const activeInfoDivRef = useRef<HTMLDivElement>(null)
@@ -147,7 +147,7 @@ const ActiveInfo = () => {
}
return (
<CommonCard
<StatisticsCard
icon={IconOxygenAnalysis}
title={
<>
@@ -183,7 +183,7 @@ const ActiveInfo = () => {
<FlexBox className={'card-content'} direction={'horizontal'}>
<div className={'big-chart'} ref={activeInfoDivRef} />
</FlexBox>
</CommonCard>
</StatisticsCard>
)
}

View File

@@ -7,7 +7,7 @@ import {
barEChartsBaseOption,
EChartsOption
} from '@/pages/System/Statistics/shared'
import { CommonCard } from '@/pages/System/Statistics'
import StatisticsCard from '@/components/system/StatisticsCard'
const CPUInfo = () => {
const keyDivRef = useRef<HTMLDivElement>(null)
@@ -142,7 +142,7 @@ const CPUInfo = () => {
return (
<>
<CommonCard
<StatisticsCard
icon={IconOxygenCpu}
title={'CPU 信息'}
loading={isLoading}
@@ -172,7 +172,7 @@ const CPUInfo = () => {
<FlexBox className={'value-chart'} ref={cpuInfoDivRef} />
<FlexBox className={'value-percent'} ref={percentDivRef} />
</FlexBox>
</CommonCard>
</StatisticsCard>
</>
)
}

View File

@@ -1,6 +1,6 @@
import { r_sys_statistics_hardware } from '@/services/system'
import FlexBox from '@/components/common/FlexBox'
import { CommonCard } from '@/pages/System/Statistics'
import StatisticsCard from '@/components/system/StatisticsCard'
const HardwareInfo = () => {
const [hardwareInfoData, setHardwareInfoData] = useState<HardwareInfoVo>()
@@ -17,7 +17,7 @@ const HardwareInfo = () => {
}, [])
return (
<CommonCard
<StatisticsCard
icon={IconOxygenHardware}
title={'硬件信息'}
loading={hardwareInfoData === undefined}
@@ -56,7 +56,7 @@ const HardwareInfo = () => {
<div title={hardwareInfoData?.disks}>{hardwareInfoData?.disks}</div>
</FlexBox>
</FlexBox>
</CommonCard>
</StatisticsCard>
)
}

View File

@@ -4,7 +4,7 @@ import { getTimesBetweenTwoTimes } from '@/util/datetime'
import { r_sys_statistics_online } from '@/services/system'
import FlexBox from '@/components/common/FlexBox'
import { getTooltipTimeFormatter, lineEChartsBaseOption } from '@/pages/System/Statistics/shared'
import { CommonCard } from '@/pages/System/Statistics'
import StatisticsCard from '@/components/system/StatisticsCard'
const OnlineInfo = () => {
const onlineInfoDivRef = useRef<HTMLDivElement>(null)
@@ -148,7 +148,7 @@ const OnlineInfo = () => {
}
return (
<CommonCard
<StatisticsCard
icon={IconOxygenOnline}
title={
<>
@@ -188,7 +188,7 @@ const OnlineInfo = () => {
<FlexBox className={'card-content'} direction={'horizontal'}>
<div className={'big-chart'} ref={onlineInfoDivRef} />
</FlexBox>
</CommonCard>
</StatisticsCard>
)
}

View File

@@ -1,7 +1,7 @@
import { utcToLocalTime } from '@/util/datetime'
import { r_sys_statistics_software } from '@/services/system'
import FlexBox from '@/components/common/FlexBox'
import { CommonCard } from '@/pages/System/Statistics'
import StatisticsCard from '@/components/system/StatisticsCard'
const SoftwareInfo = () => {
const [softwareInfoData, setSoftwareInfoData] = useState<SoftwareInfoVo>()
@@ -18,7 +18,7 @@ const SoftwareInfo = () => {
}, [])
return (
<CommonCard
<StatisticsCard
icon={IconOxygenSoftware}
title={'软件信息'}
loading={softwareInfoData === undefined}
@@ -61,7 +61,7 @@ const SoftwareInfo = () => {
</div>
</FlexBox>
</FlexBox>
</CommonCard>
</StatisticsCard>
)
}

View File

@@ -8,7 +8,7 @@ import {
barEChartsBaseOption,
EChartsOption
} from '@/pages/System/Statistics/shared'
import { CommonCard } from '@/pages/System/Statistics'
import StatisticsCard from '@/components/system/StatisticsCard'
const StorageInfo = () => {
const keyDivRef = useRef<HTMLDivElement>(null)
@@ -161,7 +161,7 @@ const StorageInfo = () => {
return (
<>
<CommonCard
<StatisticsCard
icon={IconOxygenMemory}
title={'内存信息'}
loading={isLoading}
@@ -191,7 +191,7 @@ const StorageInfo = () => {
<FlexBox className={'value-chart'} ref={storageInfoDivRef} />
<FlexBox className={'value-percent'} ref={percentDivRef} />
</FlexBox>
</CommonCard>
</StatisticsCard>
</>
)
}

View File

@@ -1,11 +1,7 @@
import { PropsWithChildren, ReactNode } from 'react'
import Icon from '@ant-design/icons'
import '@/assets/css/pages/system/statistics.scss'
import Card from '@/components/common/Card'
import FlexBox from '@/components/common/FlexBox'
import FitFullscreen from '@/components/common/FitFullscreen'
import HideScrollbar from '@/components/common/HideScrollbar'
import LoadingMask from '@/components/common/LoadingMask'
import Permission from '@/components/common/Permission'
import OnlineInfo from '@/pages/System/Statistics/OnlineInfo'
import ActiveInfo from '@/pages/System/Statistics/ActiveInfo'
@@ -14,33 +10,6 @@ import HardwareInfo from '@/pages/System/Statistics/HardwareInfo'
import CPUInfo from '@/pages/System/Statistics/CPUInfo'
import StorageInfo from '@/pages/System/Statistics/StorageInfo'
interface CommonCardProps extends PropsWithChildren {
icon: IconComponent
title: ReactNode
loading?: boolean
expand?: ReactNode
}
export const CommonCard = (props: CommonCardProps) => {
return (
<Card style={{ overflow: 'visible' }}>
<FlexBox className={'common-card'}>
<FlexBox direction={'horizontal'} className={'head'}>
<Icon component={props.icon} className={'icon'} />
<div className={'title'}>{props.title}</div>
{props.expand}
</FlexBox>
<LoadingMask
hidden={!props.loading}
maskContent={<AntdSkeleton active paragraph={{ rows: 6 }} />}
>
{props.children}
</LoadingMask>
</FlexBox>
</Card>
)
}
const Statistics = () => {
return (
<>

View File

@@ -3,6 +3,7 @@ import Icon from '@ant-design/icons'
import '@/assets/css/pages/system/tools/code.scss'
import { DATABASE_NO_RECORD_FOUND, DATABASE_SELECT_SUCCESS } from '@/constants/common.constants'
import { checkDesktop } from '@/util/common'
import { navigateToExecute, navigateToRepository } from '@/util/navigation'
import { r_sys_tool_get_one } from '@/services/system'
import { IFiles } from '@/components/Playground/shared'
import { base64ToFiles } from '@/components/Playground/files'
@@ -27,7 +28,7 @@ const Code = () => {
title: '注意',
content: '运行前请仔细查阅工具代码!',
onOk: () => {
navigate(`/system/tools/execute/${id}`)
navigateToExecute(navigate, id!)
}
})
} else {
@@ -61,9 +62,7 @@ const Code = () => {
break
case DATABASE_NO_RECORD_FOUND:
void message.error('未找到指定工具')
setTimeout(() => {
navigate('/')
}, 3000)
navigateToRepository(navigate)
break
default:
void message.error('获取工具信息失败,请稍后重试')
@@ -77,7 +76,7 @@ const Code = () => {
useEffect(() => {
getTool()
}, [])
}, [id])
return (
<>

View File

@@ -1,5 +1,6 @@
import '@/assets/css/pages/system/tools/execute.scss'
import { DATABASE_NO_RECORD_FOUND, DATABASE_SELECT_SUCCESS } from '@/constants/common.constants'
import { navigateToTools } from '@/util/navigation'
import { r_sys_tool_get_one } from '@/services/system'
import FitFullscreen from '@/components/common/FitFullscreen'
import Card from '@/components/common/Card'
@@ -24,7 +25,10 @@ const Execute = () => {
.compile(files, importMap, toolVo.entryPoint)
.then((result) => {
const output = result.outputFiles[0].text
setCompiledCode('')
setTimeout(() => {
setCompiledCode(`${output}\n${baseDist}`)
}, 100)
})
.catch((reason) => {
void message.error(`编译失败:${reason}`)
@@ -50,9 +54,7 @@ const Execute = () => {
break
case DATABASE_NO_RECORD_FOUND:
void message.error('未找到指定工具')
setTimeout(() => {
navigate('/')
}, 3000)
navigateToTools(navigate)
break
default:
void message.error('获取工具信息失败,请稍后重试')
@@ -66,7 +68,7 @@ const Execute = () => {
useEffect(() => {
getTool()
}, [])
}, [id])
return (
<FitFullscreen data-component={'system-tools-execute'}>

View File

@@ -1,5 +1,5 @@
import Icon from '@ant-design/icons'
import '@/assets/css/pages/system/tools/base.scss'
import '@/assets/css/pages/system/tools/template.scss'
import {
COLOR_PRODUCTION,
DATABASE_DELETE_SUCCESS,
@@ -1021,7 +1021,7 @@ const Template = () => {
return (
<>
<FitFullscreen data-component={'system-tools-base'}>
<FitFullscreen data-component={'system-tools-template'}>
<HideScrollbar>
<FlexBox direction={'horizontal'} className={'root-content'}>
<Card>

View File

@@ -17,6 +17,7 @@ import {
DATABASE_UPDATE_SUCCESS,
TOOL_NOT_UNDER_REVIEW
} from '@/constants/common.constants'
import { navigateToCode } from '@/util/navigation'
import FlexBox from '@/components/common/FlexBox'
import Card from '@/components/common/Card'
import FitFullscreen from '@/components/common/FitFullscreen'
@@ -196,7 +197,7 @@ const Tools = () => {
const handleOnViewBtnClick = (value: ToolVo) => {
return () => {
navigate(`/system/tools/code/${value.id}`)
navigateToCode(navigate, value.id)
}
}

View File

@@ -1,65 +1,9 @@
import { DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react'
import Icon from '@ant-design/icons'
import VanillaTilt, { TiltOptions } from 'vanilla-tilt'
import '@/assets/css/pages/system/index.scss'
import HideScrollbar from '@/components/common/HideScrollbar'
import FitFullscreen from '@/components/common/FitFullscreen'
import FlexBox from '@/components/common/FlexBox'
import Card from '@/components/common/Card'
import Permission from '@/components/common/Permission'
interface CommonCardProps
extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
icon: IconComponent
description?: ReactNode
options?: TiltOptions
url?: string
}
const CommonCard = forwardRef<HTMLDivElement, CommonCardProps>(
({
style,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
ref,
icon,
description,
options = {
reverse: true,
max: 8,
glare: true,
scale: 1.03
},
url,
children,
...props
}) => {
const navigate = useNavigate()
const cardRef = useRef<HTMLDivElement>(null)
useEffect(() => {
cardRef.current && VanillaTilt.init(cardRef.current, options)
}, [options])
const handleCardOnClick = () => {
url && navigate(url)
}
return (
<Card
style={{ overflow: 'visible', ...style }}
ref={cardRef}
{...props}
onClick={handleCardOnClick}
>
<FlexBox className={'common-card'}>
<Icon component={icon} className={'icon'} />
<div className={'text'}>{children}</div>
<div className={'description'}>{description}</div>
</FlexBox>
</Card>
)
}
)
import UrlCard from '@/components/common/UrlCard'
const System = () => {
return (
@@ -68,54 +12,54 @@ const System = () => {
<HideScrollbar isShowVerticalScrollbar autoHideWaitingTime={1000}>
<FlexBox direction={'horizontal'} className={'root-content'}>
<Permission path={'/system/statistics'}>
<CommonCard icon={IconOxygenAnalysis} url={'statistics'}>
<UrlCard icon={IconOxygenAnalysis} url={'statistics'}>
</CommonCard>
</UrlCard>
</Permission>
<Permission path={'/system/settings'}>
<CommonCard icon={IconOxygenOption} url={'settings'}>
<UrlCard icon={IconOxygenOption} url={'settings'}>
</CommonCard>
</UrlCard>
</Permission>
<Permission operationCode={['system:tool:query:tool']}>
<CommonCard icon={IconOxygenTool} url={'tools'}>
<UrlCard icon={IconOxygenTool} url={'tools'}>
</CommonCard>
</UrlCard>
</Permission>
<Permission operationCode={['system:tool:query:template']}>
<CommonCard icon={IconOxygenTemplate} url={'tools/template'}>
<UrlCard icon={IconOxygenTemplate} url={'tools/template'}>
</CommonCard>
</UrlCard>
</Permission>
<Permission operationCode={['system:tool:query:base']}>
<CommonCard icon={IconOxygenBase} url={'tools/base'}>
<UrlCard icon={IconOxygenBase} url={'tools/base'}>
</CommonCard>
</UrlCard>
</Permission>
<Permission operationCode={['system:tool:query:category']}>
<CommonCard icon={IconOxygenCategory} url={'tools/category'}>
<UrlCard icon={IconOxygenCategory} url={'tools/category'}>
</CommonCard>
</UrlCard>
</Permission>
<Permission path={'/system/user'}>
<CommonCard icon={IconOxygenUser} url={'user'}>
<UrlCard icon={IconOxygenUser} url={'user'}>
</CommonCard>
</UrlCard>
</Permission>
<Permission path={'/system/role'}>
<CommonCard icon={IconOxygenRole} url={'role'}>
<UrlCard icon={IconOxygenRole} url={'role'}>
</CommonCard>
</UrlCard>
</Permission>
<Permission path={'/system/group'}>
<CommonCard icon={IconOxygenGroup} url={'group'}>
<UrlCard icon={IconOxygenGroup} url={'group'}>
</CommonCard>
</UrlCard>
</Permission>
<Permission path={'/system/log'}>
<CommonCard icon={IconOxygenLog} url={'log'}>
<UrlCard icon={IconOxygenLog} url={'log'}>
</CommonCard>
</UrlCard>
</Permission>
</FlexBox>
</HideScrollbar>

View File

@@ -5,6 +5,7 @@ import {
DATABASE_INSERT_SUCCESS,
DATABASE_SELECT_SUCCESS
} from '@/constants/common.constants'
import { navigateToEdit } from '@/util/navigation'
import {
r_tool_category_get,
r_tool_create,
@@ -44,9 +45,7 @@ const Create = () => {
void message.success(
`创建工具 ${response.data!.name}<${response.data!.toolId}:${response.data!.platform.slice(0, 1)}${response.data!.platform.slice(1).toLowerCase()}:${response.data!.ver}> 成功`
)
navigate(
`/edit/${response.data!.toolId}${response.data!.platform !== import.meta.env.VITE_PLATFORM ? `?platform=${response.data!.platform}` : ''}`
)
navigateToEdit(navigate, response.data!.toolId, response.data!.platform)
break
case DATABASE_DUPLICATE_KEY:
void message.warning('已存在相同 ID 的应用')
@@ -330,7 +329,7 @@ const Create = () => {
label={'关键字'}
tooltip={'工具搜索每个不超过10个字符'}
name={'keywords'}
rules={[{ required: true, whitespace: true }]}
rules={[{ required: true }]}
>
<AntdSelect
mode={'tags'}
@@ -342,7 +341,7 @@ const Create = () => {
label={'类别'}
tooltip={'工具分类'}
name={'categories'}
rules={[{ required: true, whitespace: true }]}
rules={[{ required: true }]}
>
<AntdSelect
mode={'multiple'}

View File

@@ -8,6 +8,7 @@ import {
TOOL_HAS_BEEN_PUBLISHED,
TOOL_UNDER_REVIEW
} from '@/constants/common.constants'
import { navigateToRepository } from '@/util/navigation'
import { r_tool_category_get, r_tool_detail, r_tool_update } from '@/services/tool'
import { IFiles, IImportMap, ITsconfig } from '@/components/Playground/shared'
import {
@@ -117,15 +118,11 @@ const Edit = () => {
break
case TOOL_UNDER_REVIEW:
void message.error('保存失败:工具审核中')
setTimeout(() => {
navigate('/')
}, 3000)
navigateToRepository(navigate)
break
case TOOL_HAS_BEEN_PUBLISHED:
void message.error('保存失败:工具已发布')
setTimeout(() => {
navigate('/')
}, 3000)
navigateToRepository(navigate)
break
default:
void message.error('保存失败,请稍后重试')
@@ -183,15 +180,11 @@ const Edit = () => {
break
case TOOL_UNDER_REVIEW:
void message.error('保存失败:工具审核中')
setTimeout(() => {
navigate('/')
}, 3000)
navigateToRepository(navigate)
break
case TOOL_HAS_BEEN_PUBLISHED:
void message.error('保存失败:工具已发布')
setTimeout(() => {
navigate('/')
}, 3000)
navigateToRepository(navigate)
break
default:
void message.error('保存失败,请稍后重试')
@@ -244,22 +237,16 @@ const Edit = () => {
break
case 'PROCESSING':
void message.warning('工具审核中,请勿修改')
setTimeout(() => {
navigate('/')
}, 3000)
navigateToRepository(navigate)
break
default:
void message.warning('请先创建新版本后编辑工具')
setTimeout(() => {
navigate('/')
}, 3000)
navigateToRepository(navigate)
}
break
case DATABASE_NO_RECORD_FOUND:
void message.error('未找到指定工具')
setTimeout(() => {
navigate('/')
}, 3000)
navigateToRepository(navigate)
break
default:
void message.error('获取工具信息失败,请稍后重试')
@@ -322,11 +309,11 @@ const Edit = () => {
useEffect(() => {
if (!['WEB', 'DESKTOP', 'ANDROID'].includes(searchParams.get('platform')!)) {
navigate('/')
navigateToRepository(navigate)
return
}
getTool()
}, [])
}, [toolId, searchParams])
const drawerToolbar = (
<AntdSpace>

View File

@@ -1,12 +1,13 @@
import '@/assets/css/pages/tools/source.scss'
import { DATABASE_NO_RECORD_FOUND, DATABASE_SELECT_SUCCESS } from '@/constants/common.constants'
import { getLoginStatus } from '@/util/auth'
import { navigateToRepository, navigateToSource } from '@/util/navigation'
import { r_tool_detail } from '@/services/tool'
import { IFiles } from '@/components/Playground/shared'
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'
const Source = () => {
const navigate = useNavigate()
@@ -48,9 +49,7 @@ const Source = () => {
break
case DATABASE_NO_RECORD_FOUND:
void message.error('未找到指定工具')
setTimeout(() => {
navigate('/')
}, 3000)
navigateToRepository(navigate)
break
default:
void message.error('获取工具信息失败,请稍后重试')
@@ -63,26 +62,25 @@ const Source = () => {
}
useEffect(() => {
const platform = searchParams.get('platform')!
if (!['WEB', 'DESKTOP', 'ANDROID'].includes(platform)) {
navigateToRepository(navigate)
return
}
if (username === '!' && !getLoginStatus()) {
setTimeout(() => {
navigate('/')
}, 3000)
navigateToRepository(navigate)
return
}
if (username !== '!' && ver) {
navigate(`/source/${username}/${toolId}`)
navigateToSource(navigate, username!, toolId!, platform as Platform)
return
}
if (username === '!' && !ver) {
navigate(`/source/!/${toolId}/latest`)
return
}
if (!['WEB', 'DESKTOP', 'ANDROID'].includes(searchParams.get('platform')!)) {
navigate('/')
navigateToSource(navigate, '!', toolId!, platform as Platform, 'latest')
return
}
getTool()
}, [])
}, [username, toolId, ver, searchParams])
return (
<FitFullscreen data-component={'tools-source'}>

View File

@@ -1,253 +1,13 @@
import { DetailedHTMLProps, HTMLAttributes, MouseEvent, ReactNode, UIEvent } from 'react'
import VanillaTilt, { TiltOptions } from 'vanilla-tilt'
import protocolCheck from 'custom-protocol-check'
import Icon from '@ant-design/icons'
import { UIEvent } from 'react'
import '@/assets/css/pages/tools/store.scss'
import { COLOR_BACKGROUND, COLOR_MAIN, DATABASE_SELECT_SUCCESS } from '@/constants/common.constants'
import { DATABASE_SELECT_SUCCESS } from '@/constants/common.constants'
import { checkDesktop } from '@/util/common'
import { r_tool_store_get } from '@/services/tool'
import Card from '@/components/common/Card'
import FlexBox from '@/components/common/FlexBox'
import FitFullscreen from '@/components/common/FitFullscreen'
import HideScrollbar from '@/components/common/HideScrollbar'
interface CommonCardProps
extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
icon: ReactNode
toolName: string
toolId: string
toolDesc: string
options?: TiltOptions
authorName: string
authorAvatar: string
authorUsername: string
ver: string
platform: Platform
supportPlatform: Platform[]
}
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
},
authorName,
authorAvatar,
authorUsername,
ver,
platform,
supportPlatform,
...props
}: CommonCardProps) => {
const navigate = useNavigate()
const [modal, contextHolder] = AntdModal.useModal()
const cardRef = useRef<HTMLDivElement>(null)
useEffect(() => {
cardRef.current && VanillaTilt.init(cardRef.current, options)
}, [options])
const handleCardOnClick = () => {
if (!checkDesktop() && platform === 'DESKTOP') {
void message.warning('此应用需要桌面端环境,请在桌面端打开')
return
}
if (platform === 'ANDROID') {
void modal.info({
icon: <Icon style={{ color: COLOR_MAIN }} component={IconOxygenInfo} />,
title: 'Android 端',
centered: true,
maskClosable: true,
content: (
<FlexBox className={'android-qrcode'}>
<AntdQRCode
value={`oxygen://openurl/view/${authorUsername}/${toolId}`}
size={300}
/>
<AntdTag className={'tag'}>使</AntdTag>
</FlexBox>
)
})
return
}
navigate(
`/view/${authorUsername}/${toolId}${platform !== import.meta.env.VITE_PLATFORM ? `?platform=${platform}` : ''}`
)
}
const handleOnClickAuthor = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
navigate(authorUsername)
}
const handleOnSourceBtnClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
navigate(
`/source/${authorUsername}/${toolId}${platform !== import.meta.env.VITE_PLATFORM ? `?platform=${platform}` : ''}`
)
}
const handleOnAndroidBtnClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
void modal.info({
icon: <Icon style={{ color: COLOR_MAIN }} component={IconOxygenInfo} />,
title: 'Android 端',
centered: true,
maskClosable: true,
content: (
<FlexBox className={'android-qrcode'}>
<AntdQRCode
value={`oxygen://openurl/view/${authorUsername}/${toolId}`}
size={300}
/>
<AntdTag className={'tag'}>使</AntdTag>
</FlexBox>
)
})
}
const handleOnDesktopBtnClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
if (!checkDesktop()) {
void message.loading({ content: '启动桌面端中……', key: 'LOADING', duration: 0 })
protocolCheck(
`oxygen://openurl/view/${authorUsername}/${toolId}`,
() => {
void message.warning('打开失败,此应用需要桌面端环境,请安装桌面端后重试')
void message.destroy('LOADING')
},
() => {
void message.destroy('LOADING')
},
2000,
() => {
void message.warning('打开失败,此应用需要桌面端环境,请安装桌面端后重试')
void message.destroy('LOADING')
}
)
return
}
navigate(
`/view/${authorUsername}/${toolId}${platform !== import.meta.env.VITE_PLATFORM ? `?platform=${platform}` : ''}`
)
}
const handleOnWebBtnClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
navigate(
`/view/${authorUsername}/${toolId}${platform !== import.meta.env.VITE_PLATFORM ? `?platform=${platform}` : ''}`
)
}
return (
<>
<Card
style={{ overflow: 'visible', ...style }}
ref={cardRef}
{...props}
onClick={handleCardOnClick}
>
<FlexBox className={'common-card'}>
<div className={'icon'}>{icon}</div>
<div className={'version'}>
<AntdTag>
{platform.slice(0, 1)}-{ver}
</AntdTag>
</div>
<div className={'info'}>
<div className={'tool-name'}>{toolName}</div>
<div className={'tool-id'}>{`ID: ${toolId}`}</div>
{toolDesc && <div className={'tool-desc'}>{`简介:${toolDesc}`}</div>}
</div>
<div className={'author'} onClick={handleOnClickAuthor}>
<div className={'avatar'}>
<AntdAvatar
src={
<AntdImage
preview={false}
src={`data:image/png;base64,${authorAvatar}`}
alt={'Avatar'}
/>
}
style={{ background: COLOR_BACKGROUND }}
/>
</div>
<AntdTooltip title={authorUsername}>
<div className={'author-name'}>{authorName}</div>
</AntdTooltip>
</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>
</div>
</FlexBox>
</Card>
{contextHolder}
</>
)
}
interface LoadMoreCardProps {
onClick: () => void
}
const LoadMoreCard = ({ onClick }: LoadMoreCardProps) => {
const cardRef = useRef<HTMLDivElement>(null)
useEffect(() => {
cardRef.current &&
VanillaTilt.init(cardRef.current, {
reverse: true,
max: 8,
glare: true,
['max-glare']: 0.3,
scale: 1.03
})
}, [])
return (
<Card style={{ overflow: 'visible' }} ref={cardRef} onClick={onClick}>
<FlexBox className={'load-more-card'}>
<div className={'icon'}>
<Icon component={IconOxygenMore} />{' '}
</div>
<div className={'text'}></div>
</FlexBox>
</Card>
)
}
import StoreCard from '@/components/tools/StoreCard'
import LoadMoreCard from '@/components/tools/LoadMoreCard'
const Store = () => {
const scrollTopRef = useRef(0)
@@ -372,23 +132,17 @@ const Store = () => {
: webTool || desktopTool) || androidTool
return (
<CommonCard
<StoreCard
key={firstTool!.id}
icon={
<img
src={`data:image/svg+xml;base64,${firstTool!.icon}`}
alt={'Icon'}
/>
}
icon={firstTool!.icon}
toolName={firstTool!.name}
toolId={firstTool!.toolId}
toolDesc={firstTool!.description}
authorName={firstTool!.author.userInfo.nickname}
authorAvatar={firstTool!.author.userInfo.avatar}
authorUsername={firstTool!.author.username}
author={firstTool!.author}
ver={firstTool!.ver}
platform={firstTool!.platform}
supportPlatform={tools.map((value) => value.platform)}
favorite={firstTool!.favorite}
/>
)
})}

View File

@@ -1,233 +1,20 @@
import { DetailedHTMLProps, HTMLAttributes, MouseEvent, ReactNode } from 'react'
import VanillaTilt, { TiltOptions } from 'vanilla-tilt'
import protocolCheck from 'custom-protocol-check'
import Icon from '@ant-design/icons'
import '@/assets/css/pages/tools/user.scss'
import {
COLOR_BACKGROUND,
COLOR_MAIN,
DATABASE_NO_RECORD_FOUND,
DATABASE_SELECT_SUCCESS
} from '@/constants/common.constants'
import { checkDesktop } from '@/util/common'
import { navigateToRoot } from '@/util/navigation'
import { r_sys_user_info_get_basic } from '@/services/system'
import { r_tool_store_get_by_username } from '@/services/tool'
import FitFullscreen from '@/components/common/FitFullscreen'
import HideScrollbar from '@/components/common/HideScrollbar'
import Card from '@/components/common/Card'
import FlexBox from '@/components/common/FlexBox'
interface CommonCardProps
extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
icon: ReactNode
toolName: string
toolId: string
toolDesc: string
options?: TiltOptions
authorUsername: string
ver: string
platform: Platform
supportPlatform: Platform[]
}
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
},
authorUsername,
ver,
platform,
supportPlatform,
...props
}: CommonCardProps) => {
const navigate = useNavigate()
const [modal, contextHolder] = AntdModal.useModal()
const cardRef = useRef<HTMLDivElement>(null)
useEffect(() => {
cardRef.current && VanillaTilt.init(cardRef.current, options)
}, [options])
const handleCardOnClick = () => {
if (!checkDesktop() && platform === 'DESKTOP') {
void message.warning('此应用需要桌面端环境,请在桌面端打开')
return
}
if (platform === 'ANDROID') {
void modal.info({
icon: <Icon style={{ color: COLOR_MAIN }} component={IconOxygenInfo} />,
title: 'Android 端',
centered: true,
maskClosable: true,
content: (
<FlexBox className={'android-qrcode'}>
<AntdQRCode
value={`oxygen://openurl/view/${authorUsername}/${toolId}`}
size={300}
/>
<AntdTag className={'tag'}>使</AntdTag>
</FlexBox>
)
})
return
}
navigate(
`/view/${authorUsername}/${toolId}${platform !== import.meta.env.VITE_PLATFORM ? `?platform=${platform}` : ''}`
)
}
const handleOnSourceBtnClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
navigate(
`/source/${authorUsername}/${toolId}${platform !== import.meta.env.VITE_PLATFORM ? `?platform=${platform}` : ''}`
)
}
const handleOnAndroidBtnClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
void modal.info({
icon: <Icon style={{ color: COLOR_MAIN }} component={IconOxygenInfo} />,
title: 'Android 端',
centered: true,
maskClosable: true,
content: (
<FlexBox className={'android-qrcode'}>
<AntdQRCode
value={`oxygen://openurl/view/${authorUsername}/${toolId}`}
size={300}
/>
<AntdTag className={'tag'}>使</AntdTag>
</FlexBox>
)
})
}
const handleOnDesktopBtnClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
if (!checkDesktop()) {
void message.loading({ content: '启动桌面端中……', key: 'LOADING', duration: 0 })
protocolCheck(
`oxygen://openurl/view/${authorUsername}/${toolId}`,
() => {
void message.warning('打开失败,此应用需要桌面端环境,请安装桌面端后重试')
void message.destroy('LOADING')
},
() => {
void message.destroy('LOADING')
},
2000,
() => {
void message.warning('打开失败,此应用需要桌面端环境,请安装桌面端后重试')
void message.destroy('LOADING')
}
)
return
}
navigate(
`/view/${authorUsername}/${toolId}${platform !== import.meta.env.VITE_PLATFORM ? `?platform=${platform}` : ''}`
)
}
const handleOnWebBtnClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
navigate(
`/view/${authorUsername}/${toolId}${platform !== import.meta.env.VITE_PLATFORM ? `?platform=${platform}` : ''}`
)
}
return (
<>
<Card
style={{ overflow: 'visible', ...style }}
ref={cardRef}
{...props}
onClick={handleCardOnClick}
>
<FlexBox className={'common-card'}>
<div className={'icon'}>{icon}</div>
<div className={'version'}>
<AntdTag>
{platform.slice(0, 1)}-{ver}
</AntdTag>
</div>
<div className={'info'}>
<div className={'tool-name'}>{toolName}</div>
<div className={'tool-id'}>{`ID: ${toolId}`}</div>
{toolDesc && <div className={'tool-desc'}>{`简介:${toolDesc}`}</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>
</div>
</FlexBox>
</Card>
{contextHolder}
</>
)
}
interface LoadMoreCardProps {
onClick: () => void
}
const LoadMoreCard = ({ onClick }: LoadMoreCardProps) => {
const cardRef = useRef<HTMLDivElement>(null)
useEffect(() => {
cardRef.current &&
VanillaTilt.init(cardRef.current, {
reverse: true,
max: 8,
glare: true,
['max-glare']: 0.3,
scale: 1.03
})
}, [])
return (
<Card style={{ overflow: 'visible' }} ref={cardRef} onClick={onClick}>
<FlexBox className={'load-more-card'}>
<div className={'icon'}>
<Icon component={IconOxygenMore} />{' '}
</div>
<div className={'text'}></div>
</FlexBox>
</Card>
)
}
import StoreCard from '@/components/tools/StoreCard'
import LoadMoreCard from '@/components/tools/LoadMoreCard'
const User = () => {
const { username } = useParams()
@@ -269,7 +56,7 @@ const User = () => {
case DATABASE_NO_RECORD_FOUND:
void message.warning('用户不存在')
setTimeout(() => {
navigate('/')
navigateToRoot(navigate)
}, 3000)
break
default:
@@ -408,21 +195,18 @@ const User = () => {
: webTool || desktopTool) || androidTool
return (
<CommonCard
<StoreCard
key={firstTool!.id}
icon={
<img
src={`data:image/svg+xml;base64,${firstTool!.icon}`}
alt={'Icon'}
/>
}
icon={firstTool!.icon}
toolName={firstTool!.name}
toolId={firstTool!.toolId}
toolDesc={firstTool!.description}
authorUsername={firstTool!.author.username}
author={firstTool!.author}
showAuthor={false}
ver={firstTool!.ver}
platform={firstTool!.platform}
supportPlatform={tools.map((value) => value.platform)}
favorite={firstTool!.favorite}
/>
)
})}

View File

@@ -1,6 +1,7 @@
import '@/assets/css/pages/tools/view.scss'
import { DATABASE_NO_RECORD_FOUND, DATABASE_SELECT_SUCCESS } from '@/constants/common.constants'
import { getLoginStatus } from '@/util/auth'
import { navigateToRepository, navigateToRoot, navigateToView } from '@/util/navigation'
import { r_tool_detail } from '@/services/tool'
import compiler from '@/components/Playground/compiler'
import { IImportMap } from '@/components/Playground/shared'
@@ -31,7 +32,10 @@ const View = () => {
.compile(files, importMap, toolVo.entryPoint)
.then((result) => {
const output = result.outputFiles[0].text
setCompiledCode('')
setTimeout(() => {
setCompiledCode(`${output}\n${baseDist}`)
}, 100)
})
.catch((reason) => {
void message.error(`编译失败:${reason}`)
@@ -43,7 +47,10 @@ const View = () => {
try {
const baseDist = base64ToStr(toolVo.base.dist.data!)
const dist = base64ToStr(toolVo.dist.data!)
setCompiledCode('')
setTimeout(() => {
setCompiledCode(`${dist}\n${baseDist}`)
}, 100)
} catch (e) {
void message.error('载入工具失败')
}
@@ -71,9 +78,7 @@ const View = () => {
break
case DATABASE_NO_RECORD_FOUND:
void message.error('未找到指定工具')
setTimeout(() => {
navigate('/')
}, 3000)
navigateToRepository(navigate)
break
default:
void message.error('获取工具信息失败,请稍后重试')
@@ -86,26 +91,26 @@ const View = () => {
}
useEffect(() => {
const platform = searchParams.get('platform')!
if (!['WEB', 'DESKTOP', 'ANDROID'].includes(platform)) {
navigateToRepository(navigate)
return
}
if (username === '!' && !getLoginStatus()) {
setTimeout(() => {
navigate('/')
}, 3000)
void message.error('未登录')
navigateToRoot(navigate)
return
}
if (username !== '!' && ver) {
navigate(`/view/${username}/${toolId}`)
navigateToView(navigate, username!, toolId!, platform as Platform)
return
}
if (username === '!' && !ver) {
navigate(`/view/!/${toolId}/latest`)
return
}
if (!['WEB', 'DESKTOP', 'ANDROID'].includes(searchParams.get('platform')!)) {
navigate('/')
navigateToView(navigate, '!', toolId!, platform as Platform, 'latest')
return
}
getTool()
}, [])
}, [username, toolId, ver, searchParams])
return (
<FitFullscreen data-component={'tools-view'}>

View File

@@ -1,6 +1,4 @@
import { DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react'
import Icon from '@ant-design/icons'
import VanillaTilt, { TiltOptions } from 'vanilla-tilt'
import '@/assets/css/pages/tools/index.scss'
import {
DATABASE_DELETE_SUCCESS,
@@ -16,116 +14,22 @@ import {
} from '@/constants/common.constants'
import { checkDesktop } from '@/util/common'
import { getLoginStatus } from '@/util/auth'
import { navigateToEdit, navigateToSource, navigateToView } from '@/util/navigation'
import {
r_tool_cancel,
r_tool_delete,
r_tool_get,
r_tool_get_favorite,
r_tool_submit,
r_tool_upgrade
} from '@/services/tool'
import FitFullscreen from '@/components/common/FitFullscreen'
import HideScrollbar from '@/components/common/HideScrollbar'
import FlexBox from '@/components/common/FlexBox'
import Card from '@/components/common/Card'
interface CommonCardProps
extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
icon: ReactNode
toolName?: string
toolId?: string
options?: TiltOptions
url?: string
onOpen?: () => void
onEdit?: () => void
onSource?: () => void
onPublish?: () => void
onCancelReview?: () => 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,
onSource,
onPublish,
onCancelReview,
onDelete,
children,
...props
}: CommonCardProps) => {
const navigate = useNavigate()
const cardRef = useRef<HTMLDivElement>(null)
useEffect(() => {
cardRef.current && VanillaTilt.init(cardRef.current, options)
}, [options])
const handleCardOnClick = () => {
url && navigate(url)
}
return (
<Card
style={{ overflow: 'visible', ...style }}
ref={cardRef}
{...props}
onClick={handleCardOnClick}
>
<FlexBox className={'common-card'}>
<div className={'icon'}>{icon}</div>
<div className={'info'}>
{toolName && <div className={'tool-name'}>{toolName}</div>}
{toolId && <div className={'tool-id'}>{`ID: ${toolId}`}</div>}
</div>
<div className={'operation'}>
{onOpen && (
<AntdButton onClick={onOpen} size={'small'} type={'primary'}>
</AntdButton>
)}
{onEdit && onPublish && (
<div className={'edit'}>
<AntdButton.Group size={'small'}>
<AntdButton onClick={onEdit}></AntdButton>
<AntdButton onClick={onPublish}></AntdButton>
</AntdButton.Group>
</div>
)}
{onSource && (
<AntdButton size={'small'} onClick={onSource}>
</AntdButton>
)}
{onCancelReview && (
<AntdButton size={'small'} onClick={onCancelReview}>
</AntdButton>
)}
{onDelete && (
<AntdButton size={'small'} danger onClick={onDelete}>
</AntdButton>
)}
</div>
{children}
</FlexBox>
</Card>
)
}
import RepositoryCard from '@/components/tools/RepositoryCard'
import LoadMoreCard from '@/components/tools/LoadMoreCard'
import StoreCard from '@/components/tools/StoreCard'
import UrlCard from '@/components/common/UrlCard'
interface ToolCardProps {
tools: ToolVo[]
@@ -145,8 +49,12 @@ const ToolCard = ({ tools, onDelete, onUpgrade, onSubmit, onCancel }: ToolCardPr
const handleOnOpenTool = () => {
if (checkDesktop() || selectedTool.platform !== 'DESKTOP') {
navigate(
`/view/!/${selectedTool.toolId}/${selectedTool.ver}${selectedTool.platform !== import.meta.env.VITE_PLATFORM ? `?platform=${selectedTool.platform}` : ''}`
navigateToView(
navigate,
'!',
selectedTool.toolId,
selectedTool.platform,
selectedTool.ver
)
} else {
void message.warning('此应用需要桌面端环境,请在桌面端打开')
@@ -157,9 +65,7 @@ const ToolCard = ({ tools, onDelete, onUpgrade, onSubmit, onCancel }: ToolCardPr
if (['NONE', 'REJECT'].includes(selectedTool.review)) {
return () => {
if (checkDesktop() || selectedTool.platform !== 'DESKTOP') {
navigate(
`/edit/${selectedTool.toolId}${selectedTool.platform !== import.meta.env.VITE_PLATFORM ? `?platform=${selectedTool.platform}` : ''}`
)
navigateToEdit(navigate, selectedTool.toolId, selectedTool.platform)
} else {
void message.warning('此应用需要桌面端环境,请在桌面端编辑')
}
@@ -171,8 +77,12 @@ const ToolCard = ({ tools, onDelete, onUpgrade, onSubmit, onCancel }: ToolCardPr
const handleOnSourceTool = () => {
if (selectedTool.review === 'PASS') {
return () => {
navigate(
`/source/!/${selectedTool.toolId}/${selectedTool.ver}${selectedTool.platform !== import.meta.env.VITE_PLATFORM ? `?platform=${selectedTool.platform}` : ''}`
navigateToSource(
navigate,
'!',
selectedTool.toolId,
selectedTool.platform,
selectedTool.ver
)
}
}
@@ -255,10 +165,12 @@ const ToolCard = ({ tools, onDelete, onUpgrade, onSubmit, onCancel }: ToolCardPr
}
return (
<CommonCard
icon={<img src={`data:image/svg+xml;base64,${selectedTool.icon}`} alt={'Icon'} />}
<RepositoryCard
icon={selectedTool.icon}
toolName={selectedTool.name}
toolId={selectedTool.toolId}
ver={selectedTool.ver}
platform={selectedTool.platform}
onOpen={handleOnOpenTool}
onEdit={handleOnEditTool()}
onSource={handleOnSourceTool()}
@@ -289,7 +201,7 @@ const ToolCard = ({ tools, onDelete, onUpgrade, onSubmit, onCancel }: ToolCardPr
/>
</AntdTooltip>
)}
</CommonCard>
</RepositoryCard>
)
}
@@ -297,14 +209,20 @@ const Tools = () => {
const navigate = useNavigate()
const [modal, contextHolder] = AntdModal.useModal()
const [loading, setLoading] = useState(false)
const [toolData, setToolData] = useState<ToolVo[]>()
const [currentPage, setCurrentPage] = useState(0)
const [hasNextPage, setHasNextPage] = useState(false)
const [toolData, setToolData] = useState<ToolVo[]>([])
const [upgradeForm] = AntdForm.useForm<ToolUpgradeParam>()
const [currentStarPage, setCurrentStarPage] = useState(0)
const [hasNextStarPage, setHasNextStarPage] = useState(false)
const [starToolData, setStarToolData] = useState<ToolVo[]>([])
const handleOnDeleteTool = (tool: ToolVo) => {
modal
.confirm({
title: '删除',
centered: true,
maskClosable: true,
title: '删除',
content: `确定删除工具 ${tool.toolId}:${tool.platform.slice(0, 1)}${tool.platform.slice(1).toLowerCase()}:${tool.ver} 吗?`
})
.then(
@@ -317,7 +235,7 @@ const Tools = () => {
const response = res.data
if (response.code === DATABASE_DELETE_SUCCESS) {
void message.success('删除成功')
getTool()
getTool(1)
} else {
void message.error('删除失败,请稍后重试')
}
@@ -333,9 +251,9 @@ const Tools = () => {
const handleOnUpgradeTool = (tool: ToolVo) => {
void modal.confirm({
title: '更新工具',
centered: true,
maskClosable: true,
title: '更新工具',
footer: (_, { OkBtn, CancelBtn }) => (
<>
<OkBtn />
@@ -406,8 +324,10 @@ const Tools = () => {
checkDesktop() ||
response.data!.platform !== 'DESKTOP'
) {
navigate(
`/edit/${response.data!.toolId}${response.data!.platform !== import.meta.env.VITE_PLATFORM ? `?platform=${response.data!.platform}` : ''}`
navigateToEdit(
navigate,
response.data!.toolId,
response.data!.platform
)
}
resolve()
@@ -443,8 +363,9 @@ const Tools = () => {
const handleOnSubmitTool = (tool: ToolVo) => {
modal
.confirm({
title: '提交审核',
centered: true,
maskClosable: true,
title: '提交审核',
content: `确定提交审核工具 ${tool.name}:${tool.ver} 吗?`
})
.then(
@@ -458,7 +379,7 @@ const Tools = () => {
switch (response.code) {
case TOOL_SUBMIT_SUCCESS:
void message.success('提交审核成功')
getTool()
getTool(1)
break
case TOOL_UNDER_REVIEW:
void message.warning('工具审核中,请勿重复提交')
@@ -482,8 +403,9 @@ const Tools = () => {
const handleOnCancelTool = (tool: ToolVo) => {
modal
.confirm({
title: '取消审核',
centered: true,
maskClosable: true,
title: '取消审核',
content: `确定取消审核工具 ${tool.name}:${tool.ver} 吗?`
})
.then(
@@ -510,7 +432,7 @@ const Tools = () => {
})
.finally(() => {
setLoading(false)
getTool()
getTool(1)
})
}
},
@@ -518,25 +440,93 @@ const Tools = () => {
)
}
const getTool = () => {
const handleOnLoadMore = () => {
if (loading) {
return
}
getTool(currentPage + 1)
}
const handleOnLoadMoreStar = () => {
if (loading) {
return
}
getStarTool(currentStarPage + 1)
}
const getTool = (page: number) => {
if (loading) {
return
}
setLoading(true)
void message.loading({ content: '加载工具列表中', key: 'LOADING', duration: 0 })
void r_tool_get()
void r_tool_get({ currentPage: page })
.then((res) => {
const response = res.data
switch (response.code) {
case DATABASE_SELECT_SUCCESS:
setToolData(response.data!)
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(() => {
setLoading(false)
message.destroy('LOADING')
if (currentStarPage === 0) {
getStarTool(1)
}
})
}
const getStarTool = (page: number) => {
if (loading) {
return
}
setLoading(true)
void message.loading({ content: '加载收藏列表中', key: 'LOADING', duration: 0 })
void r_tool_get_favorite({ currentPage: page })
.then((res) => {
const response = res.data
switch (response.code) {
case DATABASE_SELECT_SUCCESS:
setCurrentStarPage(response.data!.current)
if (
response.data!.current === response.data!.pages ||
response.data!.total === 0
) {
setHasNextStarPage(false)
} else {
setHasNextStarPage(true)
}
if (response.data!.current === 1) {
setStarToolData(response.data!.records)
} else {
setStarToolData([...starToolData, ...response.data!.records])
}
break
default:
void message.error('加载失败,请稍后重试')
}
})
.finally(() => {
setLoading(false)
message.destroy('LOADING')
@@ -547,19 +537,18 @@ const Tools = () => {
if (!getLoginStatus()) {
return
}
getTool()
getTool(1)
}, [])
return (
<>
<FitFullscreen data-component={'tools'}>
<HideScrollbar isShowVerticalScrollbar autoHideWaitingTime={1000}>
<FlexBox direction={'horizontal'} className={'root-content'}>
<CommonCard
icon={<Icon component={IconOxygenNewProject} />}
toolName={'创建工具'}
url={'/create'}
/>
<FlexBox direction={'vertical'} className={'root-content'}>
<FlexBox direction={'horizontal'} className={'own-content'}>
<UrlCard icon={IconOxygenNewProject} url={'/create'}>
</UrlCard>
{toolData &&
Object.values(
toolData.reduce((result: Record<string, ToolVo[]>, item) => {
@@ -577,6 +566,73 @@ const Tools = () => {
onCancel={handleOnCancelTool}
/>
))}
{hasNextPage && <LoadMoreCard onClick={handleOnLoadMore} />}
</FlexBox>
{starToolData.length ? (
<>
<FlexBox className={'favorite-divider'}>
<div />
<div className={'divider-text'}></div>
<div />
</FlexBox>
<FlexBox direction={'horizontal'} className={'star-content'}>
{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 (
<StoreCard
key={firstTool!.id}
icon={firstTool!.icon}
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 && (
<LoadMoreCard onClick={handleOnLoadMoreStar} />
)}
</FlexBox>
</>
) : undefined}
</FlexBox>
</HideScrollbar>
</FitFullscreen>

View File

@@ -1,57 +1,181 @@
import { DndContext, DragOverEvent, DragStartEvent } from '@dnd-kit/core'
import { arrayMove, SortableContext } from '@dnd-kit/sortable'
import type { DragEndEvent } from '@dnd-kit/core/dist/types'
import '@/assets/css/pages/tools-framework.scss'
import { tools } from '@/router/tools'
import { checkDesktop, getToolMenuItem, saveToolMenuItem } from '@/util/common'
import { getViewPath } from '@/util/navigation'
import FitFullscreen from '@/components/common/FitFullscreen'
import Sidebar from '@/components/common/Sidebar'
import FullscreenLoadingMask from '@/components/common/FullscreenLoadingMask'
import Sortable from '@/components/dnd/Sortable'
import DragHandle from '@/components/dnd/DragHandle'
import DraggableOverlay from '@/components/dnd/DraggableOverlay'
import DropMask from '@/components/dnd/DropMask'
import Droppable from '@/components/dnd/Droppable'
const ToolsFramework = () => {
const [isDelete, setIsDelete] = useState(false)
const [toolMenuItem, setToolMenuItem] = useState<ToolMenuItem[]>(getToolMenuItem)
const [activeItem, setActiveItem] = useState<ToolMenuItem | null>(null)
const [showDropMask, setShowDropMask] = useState(false)
const handleOnDragStart = ({ active }: DragStartEvent) => {
setActiveItem(active.data.current as ToolMenuItem)
if (!active.data.current?.sortable) {
setShowDropMask(true)
}
}
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, platform }) =>
`${authorUsername}:${toolId}:${ver}:${platform}` === active.id
)
const overIndex = toolMenuItem.findIndex(
({ authorUsername, toolId, ver, platform }) =>
`${authorUsername}:${toolId}:${ver}:${platform}` === over.id
)
setToolMenuItem(arrayMove(toolMenuItem, activeIndex, overIndex))
}
if (!over) {
setToolMenuItem(
toolMenuItem.filter(
({ authorUsername, toolId, ver, platform }) =>
`${authorUsername}:${toolId}:${ver}:${platform}` !== 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
) {
if (!checkDesktop() && newItem.platform === 'DESKTOP') {
void message.warning('暂不支持添加桌面端应用')
setToolMenuItem(toolMenuItem)
} else {
setToolMenuItem([...toolMenuItem, newItem])
}
}
}
setActiveItem(null)
setShowDropMask(false)
}
const handleOnDragCancel = () => {
setActiveItem(null)
}
useEffect(() => {
saveToolMenuItem(toolMenuItem)
}, [toolMenuItem])
return (
<>
<FitFullscreen data-component={'tools-framework'} className={'flex-horizontal'}>
<DndContext
onDragStart={handleOnDragStart}
onDragOver={handleOnDragOver}
onDragEnd={handleOnDragEnd}
onDragCancel={handleOnDragCancel}
>
<div className={'left-panel'}>
<Sidebar title={'氧工具'}>
<Sidebar.ItemList>
<Sidebar.Item
end
path={'/'}
path={'/store'}
icon={tools[0].icon}
text={tools[0].name}
/>
<Sidebar.Item
end
path={'/store'}
path={'/repository'}
icon={tools[1].icon}
text={tools[1].name}
/>
</Sidebar.ItemList>
<Sidebar.Separate style={{ marginBottom: 0 }} />
<Sidebar.Separate />
<Droppable id={'menu'} className={'menu-droppable'}>
<Sidebar.Scroll>
<Sidebar.ItemList>
{tools.map((tool) => {
return tool.menu &&
tool.id !== 'tools' &&
tool.id !== 'tools-store' ? (
<Sidebar.Item
path={tool.absolutePath}
icon={tool.icon}
text={tool.name}
key={tool.id}
<SortableContext
items={toolMenuItem.map(
({ authorUsername, toolId, ver, platform }) =>
`${authorUsername}:${toolId}:${ver}:${platform}`
)}
>
{toolMenuItem.map(
({
icon,
toolName,
toolId,
authorUsername,
ver,
platform
}) => (
<Sortable
id={`${authorUsername}:${toolId}:${ver}:${platform}`}
data={{
icon,
toolName,
toolId,
authorUsername,
ver,
platform
}}
isDelete={isDelete}
>
{tool.children &&
tool.children.map((subTool) => {
return (
<Sidebar.Item
path={subTool.absolutePath}
text={subTool.name}
key={subTool.id}
path={getViewPath(
authorUsername,
toolId,
platform,
ver
)}
icon={icon}
text={toolName}
key={`${authorUsername}:${toolId}:${ver}:${platform}`}
extend={<DragHandle padding={10} />}
/>
</Sortable>
)
})}
</Sidebar.Item>
) : undefined
})}
)}
</SortableContext>
<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.Scroll>
{showDropMask && <DropMask />}
</Droppable>
</Sidebar>
</div>
<div className={'right-panel'}>
@@ -65,6 +189,7 @@ const ToolsFramework = () => {
<Outlet />
</Suspense>
</div>
</DndContext>
</FitFullscreen>
</>
)

View File

@@ -89,6 +89,8 @@ const User = () => {
changePasswordForm.resetFields()
void modal.confirm({
centered: true,
maskClosable: true,
icon: <></>,
title: (
<>
@@ -99,8 +101,6 @@ const User = () => {
</>
),
centered: true,
maskClosable: true,
footer: (_, { OkBtn, CancelBtn }) => (
<>
<OkBtn />
@@ -220,10 +220,10 @@ const User = () => {
twoFactorForm.resetFields()
if (enable) {
void modal.confirm({
title: '双因素',
centered: true,
maskClosable: true,
focusTriggerAfterClose: false,
title: '双因素',
footer: (_, { OkBtn, CancelBtn }) => (
<>
<OkBtn />
@@ -233,9 +233,9 @@ const User = () => {
content: '确定解除双因素?',
onOk: () => {
void modal.confirm({
title: '解除双因素',
centered: true,
maskClosable: true,
title: '解除双因素',
footer: (_, { OkBtn, CancelBtn }) => (
<>
<OkBtn />
@@ -313,9 +313,9 @@ const User = () => {
const response = res.data
if (response.success) {
void modal.confirm({
title: '绑定双因素',
centered: true,
maskClosable: true,
title: '绑定双因素',
footer: (_, { OkBtn, CancelBtn }) => (
<>
<OkBtn />

View File

@@ -1,14 +1,4 @@
export const tools: RouteJsonObject[] = [
{
path: '',
absolutePath: '/',
id: 'tools',
component: lazy(() => import('@/pages/Tools')),
name: '主页',
icon: lazy(() => import('~icons/oxygen/home')),
menu: true,
auth: false
},
{
path: 'store',
absolutePath: '/store',
@@ -19,6 +9,21 @@ export const tools: RouteJsonObject[] = [
icon: lazy(() => import('~icons/oxygen/store')),
menu: true
},
{
path: 'repository',
absolutePath: '/repository',
id: 'tools-repository',
component: lazy(() => import('@/pages/Tools')),
name: '个人仓库',
titlePostfix: ' - 仓库',
icon: lazy(() => import('~icons/oxygen/home')),
menu: true
},
{
path: '',
absolutePath: '/',
element: <Navigate to="/store" replace />
},
{
path: 'store/:username',
absolutePath: '/store',
@@ -34,7 +39,6 @@ export const tools: RouteJsonObject[] = [
name: '创建工具',
titlePostfix: ' - 创建新工具',
icon: lazy(() => import('~icons/oxygen/newProject')),
menu: false,
auth: true
},
{

View File

@@ -3,6 +3,7 @@ import {
URL_TOOL,
URL_TOOL_CATEGORY,
URL_TOOL_DETAIL,
URL_TOOL_FAVORITE,
URL_TOOL_STORE,
URL_TOOL_TEMPLATE
} from '@/constants/urls.constants'
@@ -19,7 +20,7 @@ export const r_tool_create = (param: ToolCreateParam) => request.post<ToolVo>(UR
export const r_tool_upgrade = (param: ToolUpgradeParam) => request.patch<ToolVo>(URL_TOOL, param)
export const r_tool_get = () => request.get<ToolVo[]>(URL_TOOL)
export const r_tool_get = (param: PageParam) => request.get<PageVo<ToolVo>>(URL_TOOL, param)
export const r_tool_detail = (username: string, toolId: string, ver: string, platform: Platform) =>
request.get<ToolVo>(`${URL_TOOL_DETAIL}/${username}/${toolId}/${ver}`, { platform })
@@ -37,3 +38,12 @@ export const r_tool_store_get = (param: ToolStoreGetParam) =>
export const r_tool_store_get_by_username = (username: string, param: ToolStoreGetParam) =>
request.get<PageVo<ToolVo>>(`${URL_TOOL_STORE}/${username}`, param)
export const r_tool_add_favorite = (param: ToolFavoriteAddRemoveParam) =>
request.post(URL_TOOL_FAVORITE, param)
export const r_tool_remove_favorite = (param: ToolFavoriteAddRemoveParam) =>
request.delete(URL_TOOL_FAVORITE, param)
export const r_tool_get_favorite = (param: PageParam) =>
request.get<PageVo<ToolVo>>(URL_TOOL_FAVORITE, param)

View File

@@ -1,6 +1,8 @@
import { createRoot } from 'react-dom/client'
import FullscreenLoadingMask from '@/components/common/FullscreenLoadingMask'
import { floor } from 'lodash'
import { STORAGE_TOOL_MENU_ITEM_KEY } from '@/constants/common.constants'
import { getLocalStorage, setLocalStorage } from '@/util/browser'
import FullscreenLoadingMask from '@/components/common/FullscreenLoadingMask'
export const randomInt = (start: number, end: number) => {
if (start > end) {
@@ -133,3 +135,22 @@ 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[]
}
export const omitText = (text: string, length: number) => {
if (text.length <= length) {
return text
}
return `${text.substring(0, length)}...`
}

127
src/util/navigation.tsx Normal file
View File

@@ -0,0 +1,127 @@
import { NavigateFunction, NavigateOptions } from 'react-router'
import { getRedirectUrl } from '@/util/route'
export const navigateToRoot = (navigate: NavigateFunction, options?: NavigateOptions) => {
navigate('/', options)
}
export const navigateToStore = (
navigate: NavigateFunction,
username?: string,
options?: NavigateOptions
) => {
navigate(`/store${username ? `/${username}` : ''}`, options)
}
export const navigateToRepository = (navigate: NavigateFunction, options?: NavigateOptions) => {
navigate('/repository', options)
}
export const navigateToLogin = (
navigate: NavigateFunction,
locationSearch?: string,
redirectUrl?: string,
options?: NavigateOptions
) => {
navigate(
redirectUrl ? getRedirectUrl('/login', redirectUrl) : `/login${locationSearch}`,
options
)
}
export const navigateToView = (
navigate: NavigateFunction,
username: string,
toolId: string,
platform: Platform,
version?: string,
options?: NavigateOptions
) => {
navigate(
`/view/${username}/${toolId}${version ? `/${version}` : ''}${platform !== import.meta.env.VITE_PLATFORM ? `?platform=${platform}` : ''}`,
options
)
}
export const navigateToSource = (
navigate: NavigateFunction,
username: string,
toolId: string,
platform: Platform,
version?: string,
options?: NavigateOptions
) => {
navigate(
`/source/${username}/${toolId}${version ? `/${version}` : ''}${platform !== import.meta.env.VITE_PLATFORM ? `?platform=${platform}` : ''}`,
options
)
}
export const navigateToRedirect = (
navigate: NavigateFunction,
searchParams: URLSearchParams,
defaultUrl: '/repository' | '/',
options?: NavigateOptions
) => {
navigate(searchParams.get('redirect') ?? defaultUrl, options)
}
export const navigateToForget = (
navigate: NavigateFunction,
locationSearch: string,
options?: NavigateOptions
) => {
navigate(`/forget/${locationSearch}`, options)
}
export const navigateToRegister = (
navigate: NavigateFunction,
locationSearch: string,
options?: NavigateOptions
) => {
navigate(`/register/${locationSearch}`, options)
}
export const navigateToExecute = (
navigate: NavigateFunction,
toolId: string,
options?: NavigateOptions
) => {
navigate(`/system/tools/execute/${toolId}`, options)
}
export const navigateToCode = (
navigate: NavigateFunction,
toolId: string,
options?: NavigateOptions
) => {
navigate(`/system/tools/code/${toolId}`, options)
}
export const navigateToEdit = (
navigate: NavigateFunction,
toolId: string,
platform: Platform,
options?: NavigateOptions
) => {
navigate(
`/edit/${toolId}${platform !== import.meta.env.VITE_PLATFORM ? `?platform=${platform}` : ''}`,
options
)
}
export const navigateToUser = (navigate: NavigateFunction, options?: NavigateOptions) => {
navigate('/user', options)
}
export const navigateToTools = (navigate: NavigateFunction, options?: NavigateOptions) => {
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}` : ''}`