Refactor:11; Fix:6; Chore:1; Feat:7; Build:1 #49
@@ -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
3406
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
63
package.json
63
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
font-size: constants.$SIZE_ICON_SM;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
span {
|
||||
transform: rotateZ(180deg);
|
||||
transition: all .3s;
|
||||
@@ -55,16 +56,18 @@
|
||||
.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 {
|
||||
> .menu-bt {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
height: 40px;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -148,8 +166,8 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
>.menu-bt {
|
||||
a {
|
||||
> .menu-bt {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +256,7 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
a{
|
||||
a {
|
||||
color: constants.$main-color;
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -282,7 +318,7 @@
|
||||
padding-left: 6px;
|
||||
left: 100%;
|
||||
z-index: 1000;
|
||||
box-shadow: 5px 5px 15px 0 rgba(0,0,0,0.1);
|
||||
box-shadow: 5px 5px 15px 0 rgba(0, 0, 0, 0.1);
|
||||
|
||||
.content {
|
||||
padding: 8px;
|
||||
@@ -291,6 +327,7 @@
|
||||
|
||||
.icon-exit {
|
||||
padding: 4px 8px;
|
||||
|
||||
&:hover {
|
||||
border-radius: 8px;
|
||||
background-color: constants.$background-color;
|
||||
@@ -299,7 +336,7 @@
|
||||
}
|
||||
|
||||
&.hide {
|
||||
display: none!important;
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
28
src/assets/css/components/common/url-card.scss
Normal file
28
src/assets/css/components/common/url-card.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
5
src/assets/css/components/dnd/drag-handle.scss
Normal file
5
src/assets/css/components/dnd/drag-handle.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
[data-component=component-drag-handle] {
|
||||
background-color: transparent;
|
||||
color: unset;
|
||||
cursor: grab;
|
||||
}
|
||||
30
src/assets/css/components/dnd/drop-mask.scss
Normal file
30
src/assets/css/components/dnd/drop-mask.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
33
src/assets/css/components/system/setting-card.scss
Normal file
33
src/assets/css/components/system/setting-card.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/assets/css/components/system/statistics-card.scss
Normal file
78
src/assets/css/components/system/statistics-card.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/assets/css/components/tools/load-more-card.scss
Normal file
27
src/assets/css/components/tools/load-more-card.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/assets/css/components/tools/repository-card.scss
Normal file
51
src/assets/css/components/tools/repository-card.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
110
src/assets/css/components/tools/store-card.scss
Normal file
110
src/assets/css/components/tools/store-card.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
}
|
||||
|
||||
>*:first-child {
|
||||
width: 0;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
|
||||
43
src/assets/css/pages/system/tools/template.scss
Normal file
43
src/assets/css/pages/system/tools/template.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
1
src/assets/svg/handle.svg
Normal file
1
src/assets/svg/handle.svg
Normal 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 |
1
src/assets/svg/receive.svg
Normal file
1
src/assets/svg/receive.svg
Normal 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
1
src/assets/svg/star.svg
Normal 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 |
1
src/assets/svg/starFilled.svg
Normal file
1
src/assets/svg/starFilled.svg
Normal 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 |
@@ -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'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -4,7 +4,7 @@ const Separate = ({
|
||||
className,
|
||||
...props
|
||||
}: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>) => {
|
||||
return <div className={`separate ${className ? ` ${className}` : ''}`} {...props} />
|
||||
return <div className={`separate${className ? ` ${className}` : ''}`} {...props} />
|
||||
}
|
||||
|
||||
export default Separate
|
||||
|
||||
57
src/components/common/UrlCard.tsx
Normal file
57
src/components/common/UrlCard.tsx
Normal 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
|
||||
27
src/components/dnd/DragHandle.tsx
Normal file
27
src/components/dnd/DragHandle.tsx
Normal 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
|
||||
47
src/components/dnd/Draggable.tsx
Normal file
47
src/components/dnd/Draggable.tsx
Normal 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
|
||||
22
src/components/dnd/DraggableOverlay.tsx
Normal file
22
src/components/dnd/DraggableOverlay.tsx
Normal 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
|
||||
14
src/components/dnd/DropMask.tsx
Normal file
14
src/components/dnd/DropMask.tsx
Normal 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
|
||||
16
src/components/dnd/Droppable.tsx
Normal file
16
src/components/dnd/Droppable.tsx
Normal 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
|
||||
13
src/components/dnd/HandleContext.ts
Normal file
13
src/components/dnd/HandleContext.ts
Normal 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() {}
|
||||
})
|
||||
54
src/components/dnd/Sortable.tsx
Normal file
54
src/components/dnd/Sortable.tsx
Normal 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
|
||||
48
src/components/system/SettingCard.tsx
Normal file
48
src/components/system/SettingCard.tsx
Normal 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
|
||||
35
src/components/system/StatisticsCard.tsx
Normal file
35
src/components/system/StatisticsCard.tsx
Normal 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
|
||||
42
src/components/tools/LoadMoreCard.tsx
Normal file
42
src/components/tools/LoadMoreCard.tsx
Normal 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
|
||||
115
src/components/tools/RepositoryCard.tsx
Normal file
115
src/components/tools/RepositoryCard.tsx
Normal 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
|
||||
312
src/components/tools/StoreCard.tsx
Normal file
312
src/components/tools/StoreCard.tsx
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
16
src/global.d.ts
vendored
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
>
|
||||
注册
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
>
|
||||
登录
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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[]>()
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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'}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'}>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -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'}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
127
src/util/navigation.tsx
Normal 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}` : ''}`
|
||||
Reference in New Issue
Block a user