Merge pull request 'v1.0-230926' (#27) from dev into master

Reviewed-on: FatttSnake/fatweb-ui#27
This commit was merged in pull request #27.
This commit is contained in:
FatttSnake
2023-09-26 11:06:04 +08:00
52 changed files with 1951 additions and 496 deletions

View File

@@ -24,11 +24,10 @@ module.exports = {
rules: {
'no-cond-assign': 'error',
'eqeqeq': 'error',
'indent': ['error', 4, { 'SwitchCase': 1 }],
'prettier/prettier': [
'error',
{
endOfLine: 'auto'
endOfLine: 'auto',
}
],
'react-refresh/only-export-components': [

View File

@@ -2,9 +2,9 @@
<html lang="zh">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<title>FatWeb</title>
</head>
<body>
<div id="root"></div>

193
package-lock.json generated
View File

@@ -8,8 +8,8 @@
"name": "fatweb-ui",
"version": "0.0.0",
"dependencies": {
"@ant-design/icons": "^5.2.5",
"antd": "^5.8.5",
"@ant-design/icons": "^5.2.6",
"antd": "^5.8.6",
"axios": "^1.5.0",
"jwt-decode": "^3.1.2",
"localforage": "^1.10.0",
@@ -18,19 +18,18 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^6.15.0",
"react-router-dom": "^6.15.0",
"sort-by": "^0.0.2"
"react-router-dom": "^6.15.0"
},
"devDependencies": {
"@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0",
"@types/jsdom": "^21.1.2",
"@types/lodash": "^4.14.197",
"@types/node": "^20.5.7",
"@types/lodash": "^4.14.198",
"@types/node": "^20.5.9",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.5.0",
"@typescript-eslint/parser": "^6.5.0",
"@typescript-eslint/eslint-plugin": "^6.6.0",
"@typescript-eslint/parser": "^6.6.0",
"@vitejs/plugin-react": "^4.0.4",
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
@@ -42,10 +41,11 @@
"eslint-plugin-react-refresh": "^0.4.3",
"jsdom": "^22.1.0",
"prettier": "^3.0.3",
"sass": "^1.66.1",
"stylelint-config-prettier": "^9.0.5",
"typescript": "^5.2.2",
"unplugin-auto-import": "^0.16.6",
"unplugin-icons": "^0.16.6",
"unplugin-icons": "^0.17.0",
"vite": "^4.4.9"
}
},
@@ -1508,7 +1508,7 @@
},
"node_modules/@types/json-schema": {
"version": "7.0.12",
"resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.12.tgz",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
"integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==",
"dev": true
},
@@ -1519,9 +1519,9 @@
"dev": true
},
"node_modules/@types/lodash": {
"version": "4.14.197",
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.14.197.tgz",
"integrity": "sha512-BMVOiWs0uNxHVlHBgzTIqJYmj+PgCo4euloGF+5m4okL3rEYzM2EEv78mw8zWSMM57dM7kVIgJ2QDvwHSoCI5g==",
"version": "4.14.198",
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.14.198.tgz",
"integrity": "sha512-trNJ/vtMZYMLhfN45uLq4ShQSw0/S7xCTLLVM+WM1rmFpba/VS42jVUgaO3w/NOLiWR/09lnYk0yMaA/atdIsg==",
"dev": true
},
"node_modules/@types/minimist": {
@@ -1585,7 +1585,7 @@
},
"node_modules/@types/semver": {
"version": "7.5.1",
"resolved": "https://registry.npmmirror.com/@types/semver/-/semver-7.5.1.tgz",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.1.tgz",
"integrity": "sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg==",
"dev": true
},
@@ -1596,16 +1596,16 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "6.5.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.5.0.tgz",
"integrity": "sha512-2pktILyjvMaScU6iK3925uvGU87E+N9rh372uGZgiMYwafaw9SXq86U04XPq3UH6tzRvNgBsub6x2DacHc33lw==",
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.6.0.tgz",
"integrity": "sha512-CW9YDGTQnNYMIo5lMeuiIG08p4E0cXrXTbcZ2saT/ETE7dWUrNxlijsQeU04qAAKkILiLzdQz+cGFxCJjaZUmA==",
"dev": true,
"dependencies": {
"@eslint-community/regexpp": "^4.5.1",
"@typescript-eslint/scope-manager": "6.5.0",
"@typescript-eslint/type-utils": "6.5.0",
"@typescript-eslint/utils": "6.5.0",
"@typescript-eslint/visitor-keys": "6.5.0",
"@typescript-eslint/scope-manager": "6.6.0",
"@typescript-eslint/type-utils": "6.6.0",
"@typescript-eslint/utils": "6.6.0",
"@typescript-eslint/visitor-keys": "6.6.0",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.2.4",
@@ -1616,6 +1616,10 @@
"engines": {
"node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
"eslint": "^7.0.0 || ^8.0.0"
@@ -1660,20 +1664,24 @@
"dev": true
},
"node_modules/@typescript-eslint/parser": {
"version": "6.5.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-6.5.0.tgz",
"integrity": "sha512-LMAVtR5GN8nY0G0BadkG0XIe4AcNMeyEy3DyhKGAh9k4pLSMBO7rF29JvDBpZGCmp5Pgz5RLHP6eCpSYZJQDuQ==",
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.6.0.tgz",
"integrity": "sha512-setq5aJgUwtzGrhW177/i+DMLqBaJbdwGj2CPIVFFLE0NCliy5ujIdLHd2D1ysmlmsjdL2GWW+hR85neEfc12w==",
"dev": true,
"dependencies": {
"@typescript-eslint/scope-manager": "6.5.0",
"@typescript-eslint/types": "6.5.0",
"@typescript-eslint/typescript-estree": "6.5.0",
"@typescript-eslint/visitor-keys": "6.5.0",
"@typescript-eslint/scope-manager": "6.6.0",
"@typescript-eslint/types": "6.6.0",
"@typescript-eslint/typescript-estree": "6.6.0",
"@typescript-eslint/visitor-keys": "6.6.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^7.0.0 || ^8.0.0"
},
@@ -1684,32 +1692,40 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "6.5.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-6.5.0.tgz",
"integrity": "sha512-A8hZ7OlxURricpycp5kdPTH3XnjG85UpJS6Fn4VzeoH4T388gQJ/PGP4ole5NfKt4WDVhmLaQ/dBLNDC4Xl/Kw==",
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.6.0.tgz",
"integrity": "sha512-pT08u5W/GT4KjPUmEtc2kSYvrH8x89cVzkA0Sy2aaOUIw6YxOIjA8ilwLr/1fLjOedX1QAuBpG9XggWqIIfERw==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "6.5.0",
"@typescript-eslint/visitor-keys": "6.5.0"
"@typescript-eslint/types": "6.6.0",
"@typescript-eslint/visitor-keys": "6.6.0"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "6.5.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-6.5.0.tgz",
"integrity": "sha512-f7OcZOkRivtujIBQ4yrJNIuwyCQO1OjocVqntl9dgSIZAdKqicj3xFDqDOzHDlGCZX990LqhLQXWRnQvsapq8A==",
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.6.0.tgz",
"integrity": "sha512-8m16fwAcEnQc69IpeDyokNO+D5spo0w1jepWWY2Q6y5ZKNuj5EhVQXjtVAeDDqvW6Yg7dhclbsz6rTtOvcwpHg==",
"dev": true,
"dependencies": {
"@typescript-eslint/typescript-estree": "6.5.0",
"@typescript-eslint/utils": "6.5.0",
"@typescript-eslint/typescript-estree": "6.6.0",
"@typescript-eslint/utils": "6.6.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.0.1"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^7.0.0 || ^8.0.0"
},
@@ -1720,22 +1736,26 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "6.5.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-6.5.0.tgz",
"integrity": "sha512-eqLLOEF5/lU8jW3Bw+8auf4lZSbbljHR2saKnYqON12G/WsJrGeeDHWuQePoEf9ro22+JkbPfWQwKEC5WwLQ3w==",
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.6.0.tgz",
"integrity": "sha512-CB6QpJQ6BAHlJXdwUmiaXDBmTqIE2bzGTDLADgvqtHWuhfNP3rAOK7kAgRMAET5rDRr9Utt+qAzRBdu3AhR3sg==",
"dev": true,
"engines": {
"node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "6.5.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.5.0.tgz",
"integrity": "sha512-q0rGwSe9e5Kk/XzliB9h2LBc9tmXX25G0833r7kffbl5437FPWb2tbpIV9wAATebC/018pGa9fwPDuvGN+LxWQ==",
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.6.0.tgz",
"integrity": "sha512-hMcTQ6Al8MP2E6JKBAaSxSVw5bDhdmbCEhGW/V8QXkb9oNsFkA4SBuOMYVPxD3jbtQ4R/vSODBsr76R6fP3tbA==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "6.5.0",
"@typescript-eslint/visitor-keys": "6.5.0",
"@typescript-eslint/types": "6.6.0",
"@typescript-eslint/visitor-keys": "6.6.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -1745,6 +1765,10 @@
"engines": {
"node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
@@ -1753,7 +1777,7 @@
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
@@ -1765,7 +1789,7 @@
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
"version": "7.5.4",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.5.4.tgz",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"dependencies": {
@@ -1780,34 +1804,38 @@
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/@typescript-eslint/utils": {
"version": "6.5.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-6.5.0.tgz",
"integrity": "sha512-9nqtjkNykFzeVtt9Pj6lyR9WEdd8npPhhIPM992FWVkZuS6tmxHfGVnlUcjpUP2hv8r4w35nT33mlxd+Be1ACQ==",
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.6.0.tgz",
"integrity": "sha512-mPHFoNa2bPIWWglWYdR0QfY9GN0CfvvXX1Sv6DlSTive3jlMTUy+an67//Gysc+0Me9pjitrq0LJp0nGtLgftw==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0",
"@typescript-eslint/scope-manager": "6.5.0",
"@typescript-eslint/types": "6.5.0",
"@typescript-eslint/typescript-estree": "6.5.0",
"@typescript-eslint/scope-manager": "6.6.0",
"@typescript-eslint/types": "6.6.0",
"@typescript-eslint/typescript-estree": "6.6.0",
"semver": "^7.5.4"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^7.0.0 || ^8.0.0"
}
},
"node_modules/@typescript-eslint/utils/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
@@ -1819,7 +1847,7 @@
},
"node_modules/@typescript-eslint/utils/node_modules/semver": {
"version": "7.5.4",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.5.4.tgz",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"dependencies": {
@@ -1834,21 +1862,25 @@
},
"node_modules/@typescript-eslint/utils/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "6.5.0",
"resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.5.0.tgz",
"integrity": "sha512-yCB/2wkbv3hPsh02ZS8dFQnij9VVQXJMN/gbQsaaY+zxALkZnxa/wagvLEFsAWMPv7d7lxQmNsIzGU1w/T/WyA==",
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.6.0.tgz",
"integrity": "sha512-L61uJT26cMOfFQ+lMZKoJNbAEckLe539VhTxiGHrWl5XSKQgA0RTBZJW2HFPy5T0ZvPVSD93QsrTKDkfNwJGyQ==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "6.5.0",
"@typescript-eslint/types": "6.6.0",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@vitejs/plugin-react": {
@@ -4018,6 +4050,12 @@
"resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"node_modules/immutable": {
"version": "4.3.4",
"resolved": "https://registry.npmmirror.com/immutable/-/immutable-4.3.4.tgz",
"integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==",
"dev": true
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -6444,6 +6482,23 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
"node_modules/sass": {
"version": "1.66.1",
"resolved": "https://registry.npmmirror.com/sass/-/sass-1.66.1.tgz",
"integrity": "sha512-50c+zTsZOJVgFfTgwwEzkjA3/QACgdNsKueWPyAR0mRINIvLAStVQBbPg14iuqEQ74NPDbXzJARJ/O4SI1zftA==",
"dev": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/saxes/-/saxes-6.0.0.tgz",
@@ -6592,11 +6647,6 @@
"tslib": "^2.0.3"
}
},
"node_modules/sort-by": {
"version": "0.0.2",
"resolved": "https://registry.npmmirror.com/sort-by/-/sort-by-0.0.2.tgz",
"integrity": "sha512-iOX5oHA4a0eqTMFiWrHYqv924UeRKFBLhym7iwSVG37Egg2wApgZKAjyzM9WZjMwKv6+8Zi+nIaJ7FYsO9EkoA=="
},
"node_modules/source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz",
@@ -7340,9 +7390,9 @@
}
},
"node_modules/unplugin-icons": {
"version": "0.16.6",
"resolved": "https://registry.npmmirror.com/unplugin-icons/-/unplugin-icons-0.16.6.tgz",
"integrity": "sha512-jL70sAC7twp4hI/MTfm+vyvTRtHqiEIzf3XOjJz7yzhMEEQnk5Ey5YIXRAU03Mc4BF99ITvvnBzfyRZee86OeA==",
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/unplugin-icons/-/unplugin-icons-0.17.0.tgz",
"integrity": "sha512-gMv66eY/Hj64heM55XrfDH3LUCWI51mtkBVUPVl9VkpvLgAYhdVe9nRuzu6p+idmCLSQVq7xiPxQcD4aXCgW5A==",
"dev": true,
"dependencies": {
"@antfu/install-pkg": "^0.1.1",
@@ -7353,6 +7403,9 @@
"local-pkg": "^0.4.3",
"unplugin": "^1.4.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@svgr/core": ">=7.0.0",
"@svgx/core": "^1.0.1",

View File

@@ -6,14 +6,15 @@
"scripts": {
"dev": "vite",
"dev-host": "vite --host 0.0.0.0",
"build": "vite build && tsc && vite build",
"build": "vite build",
"clean": "rimraf dist .eslintrc-auto-import.json auto-imports.d.ts",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"format": "prettier --write src/",
"preview": "vite preview"
},
"dependencies": {
"@ant-design/icons": "^5.2.5",
"antd": "^5.8.5",
"@ant-design/icons": "^5.2.6",
"antd": "^5.8.6",
"axios": "^1.5.0",
"jwt-decode": "^3.1.2",
"localforage": "^1.10.0",
@@ -22,19 +23,18 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^6.15.0",
"react-router-dom": "^6.15.0",
"sort-by": "^0.0.2"
"react-router-dom": "^6.15.0"
},
"devDependencies": {
"@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0",
"@types/jsdom": "^21.1.2",
"@types/lodash": "^4.14.197",
"@types/node": "^20.5.7",
"@types/lodash": "^4.14.198",
"@types/node": "^20.5.9",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.5.0",
"@typescript-eslint/parser": "^6.5.0",
"@typescript-eslint/eslint-plugin": "^6.6.0",
"@typescript-eslint/parser": "^6.6.0",
"@vitejs/plugin-react": "^4.0.4",
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
@@ -46,10 +46,11 @@
"eslint-plugin-react-refresh": "^0.4.3",
"jsdom": "^22.1.0",
"prettier": "^3.0.3",
"sass": "^1.66.1",
"stylelint-config-prettier": "^9.0.5",
"typescript": "^5.2.2",
"unplugin-auto-import": "^0.16.6",
"unplugin-icons": "^0.16.6",
"unplugin-icons": "^0.17.0",
"vite": "^4.4.9"
}
}

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

1
public/logo.svg Normal file
View File

@@ -0,0 +1 @@
<svg id="logo" xmlns="http://www.w3.org/2000/svg" width="911.75" height="898.91" viewBox="0 0 911.75 898.91"><defs><linearGradient id="FillColor" x1="0" x2="50%" y1="50%" y2="0"><stop offset="0%" stop-color="#2af598"/><stop offset="100%" stop-color="#009efd"/></linearGradient></defs><path fill="url(#FillColor)" d="M828.52,831.72c-216,259.7-774.46,115.85-765.17-319.5-19-292.76,370.82-602.09,704.81-367.11,44.53,48.2,81.39,32.52,121.28,46.67,28.28,14.3,21.36,42.61,52.36,142.6,31.6,101.09,60.92,133.92-17.45,182.4-28,11.34-46.6-2.34-117.82-25.31-92-29.65-109-22.33-132.66-48-27.26-29.62-14.42-50.13-41.31-80.13C507.2,259.81,336.39,351,290.74,489.72c-7,26.59-20.37,92.15,8.72,162.38,25.72,62.11,71.11,95.23,99.93,115.7C554.85,878.2,781.33,840.41,828.52,831.72Zm50.11-584.95c-15.4,5.93.73,47,16,40.82C910.07,281.66,893.94,240.63,878.63,246.77Zm-152,180.06c-4.34,15.92,35.73,26.64,39.92,10.68C770.9,421.58,730.83,410.86,726.63,426.83Z" transform="translate(-62.68 -62.16)"/></svg>

After

Width:  |  Height:  |  Size: 978 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,42 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@@ -1,10 +1,13 @@
import React from 'react'
import router from '@/router'
import LoadingMask from '@/components/common/LoadingMask.tsx'
const App: React.FC = () => {
return (
<>
<RouterProvider router={router} />
<Suspense fallback={<LoadingMask />}>
<RouterProvider router={router} />
</Suspense>
</>
)
}

View File

@@ -0,0 +1,22 @@
@mixin keyframes($animationName) {
@-webkit-keyframes #{$animationName} {
@content
}
@-moz-keyframes #{$animationName} {
@content
}
@-o-keyframes #{$animationName} {
@content
}
@keyframes #{$animationName} {
@content
}
}
@mixin unique-keyframes {
$animationName: unique-id();
animation-name: $animationName;
@include keyframes($animationName) {
@content
}
}

View File

@@ -1,63 +0,0 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
em,
i {
font-style: normal
}
li {
list-style: none
}
img {
border: 0;
vertical-align: middle
}
button {
cursor: pointer
}
a {
color: #666;
text-decoration: none
}
button,
input {
font-family: Microsoft YaHei, Heiti SC, tahoma, arial, Hiragino Sans GB, "\5B8B\4F53", sans-serif;
border: 0;
outline: none;
}
body {
-webkit-font-smoothing: antialiased;
background-color: #fff;
font: 12px/1.5 Microsoft YaHei, Heiti SC, tahoma, arial, Hiragino Sans GB, "\5B8B\4F53", sans-serif;
color: #666
}
.hide,
.none {
display: none
}
.clearfix:after {
visibility: hidden;
clear: both;
display: block;
content: ".";
height: 0
}
.clearfix {
*zoom: 1
}

63
src/assets/css/base.scss Normal file
View File

@@ -0,0 +1,63 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
em,
i {
font-style: normal
}
li {
list-style: none
}
img {
border: 0;
vertical-align: middle
}
button {
cursor: pointer
}
a {
color: #666;
text-decoration: none
}
button,
input {
font-family: Microsoft YaHei, Heiti SC, tahoma, arial, Hiragino Sans GB, "\5B8B\4F53", sans-serif;
border: 0;
outline: none;
}
body {
-webkit-font-smoothing: antialiased;
background-color: #fff;
font: 12px/1.5 Microsoft YaHei, Heiti SC, tahoma, arial, Hiragino Sans GB, "\5B8B\4F53", sans-serif;
color: #666
}
.hide,
.none {
display: none
}
.clearfix:after {
visibility: hidden;
clear: both;
display: block;
content: ".";
height: 0
}
.clearfix {
*zoom: 1
}

View File

@@ -1,106 +0,0 @@
:root {
--main-color: #00D4FF;
--background-color: #F5F5F5;
--font-main-color: #4D4D4D;
--font-secondary-color: #9E9E9E;
}
.body {
color: var(--font-main-color);
user-select: none;
min-width: 1800px;
min-height: 400px;
}
.fill {
height: 100%;
width: 100%;
}
.fill-with {
width: 100%;
}
.fill-height {
height: 100%;
}
.background-white {
background-color: white;
}
.center-box {
display: flex;
justify-content: center;
align-items: center;
}
.vertical-center-box {
display: flex;
align-items: center;
}
.horizontal-center-box {
display: flex;
justify-content: center;
}
.icon-size-xs {
width: 16px;
height: 16px;
}
.icon-size-xs > use {
width: 16px;
height: 16px;
}
.icon-size-sm {
width: 20px;
height: 20px;
}
.icon-size-sm > use {
width: 20px;
height: 20px;
}
.icon-size-md {
width: 24px;
height: 24px;
}
.icon-size-md > use {
width: 24px;
height: 24px;
}
.icon-size-lg {
width: 32px;
height: 32px;
}
.icon-size-lg > use {
width: 32px;
height: 32px;
}
.icon-size-xl {
width: 64px;
height: 64px;
}
.icon-size-xl > use {
width: 64px;
height: 64px;
}
.icon-size-menu {
width: 23px;
height: 23px;
}
.icon-size-menu > use {
width: 23px;
height: 23px;
}

115
src/assets/css/common.scss Normal file
View File

@@ -0,0 +1,115 @@
@use '@/assets/css/constants.scss' as constants;
#root {
height: 100vh;
width: 100vw;
}
.body {
color: constants.$font-main-color;
user-select: none;
min-width: 900px;
min-height: 400px;
}
.fill {
height: 100%;
width: 100%;
}
.fill-with {
width: 100%;
}
.fill-height {
height: 100%;
}
.background-white {
background-color: white;
}
.center-box {
display: flex;
justify-content: center;
align-items: center;
}
.vertical-center-box {
display: flex;
align-items: center;
}
.horizontal-center-box {
display: flex;
justify-content: center;
}
.icon-size-xs {
width: 16px;
height: 16px;
> use {
width: 16px;
height: 16px;
}
}
.icon-size-sm {
width: 20px;
height: 20px;
> use {
width: 20px;
height: 20px;
}
}
.icon-size-md {
width: 24px;
height: 24px;
> use {
width: 24px;
height: 24px;
}
}
.icon-size-lg {
width: 32px;
height: 32px;
> use {
width: 32px;
height: 32px;
}
}
.icon-size-xl {
width: 64px;
height: 64px;
> use {
width: 64px;
height: 64px;
}
}
.icon-size-menu {
width: 23px;
height: 23px;
> use {
width: 23px;
height: 23px;
}
}
.flex-horizontal {
flex-direction: row;
}
.flex-vertical {
flex-direction: column;
}

View File

@@ -0,0 +1,7 @@
.fit-center {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}

View File

@@ -0,0 +1,5 @@
.fit-fullscreen {
position: relative;
width: 100%;
height: 100vh;
}

View File

@@ -0,0 +1,68 @@
@use '@/assets/css/constants' as constants;
.hide-scrollbar-mask {
width: 100%;
height: 100%;
overflow: hidden;
.hide-scrollbar-selection {
position: relative;
overflow: scroll;
scrollbar-width: none;
-ms-overflow-style: none;
.hide-scrollbar-content {
display: inline-block;
width: 100%;
min-width: 900px;
}
}
::-webkit-scrollbar {
display: none;
}
.scrollbar {
position: absolute;
z-index: 1000;
opacity: .5;
touch-action: none;
.box {
position: relative;
width: 100%;
height: 100%;
border-radius: 8px;
overflow: hidden;
.block {
position: absolute;
width: 100%;
height: 100%;
border-radius: 8px;
background-color: constants.$font-secondary-color;
transition: background-color .2s;
}
:hover {
background-color: constants.$font-main-color;
}
}
}
.vertical-scrollbar {
padding: 12px 4px;
width: 16px;
height: 100%;
right: 0;
top: 0;
}
.horizontal-scrollbar {
padding: 4px 12px;
width: 100%;
height: 16px;
left: 0;
bottom: 0;
}
}

View File

@@ -0,0 +1,34 @@
@use '@/assets/css/constants.scss' as constants;
.dot-list {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
.item {
flex: auto;
cursor: pointer;
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
border: {
width: 2px;
color: constants.$font-secondary-color;
style: solid;
};
transition: all .2s;
}
:hover {
background-color: constants.$focus-color;
}
}
.active > * {
background-color: constants.$font-secondary-color !important;
}
}

View File

@@ -0,0 +1,10 @@
.loading-mask {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
z-index: 100;
background-color: rgba(200, 200, 200, 0.2);
}

View File

@@ -0,0 +1,19 @@
.icons {
display: flex;
gap: 20px;
.icon {
font-size: 8em;
color: white;
}
}
.links {
font-size: 2em;
text-decoration: underline;
color: white;
> * {
color: white;
}
}

View File

@@ -0,0 +1,11 @@
.indicator {
position: fixed;
margin: {
right: 20px;
};
width: 20px;
height: 100px;
right: 0;
top: 50%;
transform: translateY(-50%);
}

View File

@@ -0,0 +1,63 @@
@use "@/assets/css/mixins" as mixins;
.center-box {
display: flex;
flex-direction: column;
}
.big-logo {
font: {
size: 5em;
weight: bold;
};
color: #666;
}
.slogan {
font: {
size: 1.3em;
style: italic;
};
color: #666;
.cursor {
font-style: normal;
animation: 1s infinite;
@include mixins.unique-keyframes {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
}
}
.scroll-down {
position: absolute;
bottom: 10px;
padding: 20px;
cursor: pointer;
animation: 1.5s infinite;
@include mixins.unique-keyframes {
0%,
100% {
-ms-filter: none;
filter: none;
opacity: 1;
transform: translateY(10px);
}
50% {
-ms-filter: alpha(opacity=40);
filter: alpha(opacity=40);
opacity: .4;
transform: translateY(-10px);
}
}
}

View File

@@ -0,0 +1,8 @@
$main-color: #00D4FF;
$background-color: #F5F5F5;
$font-main-color: #4D4D4D;
$font-secondary-color: #9E9E9E;
$focus-color: #DDDDDD;
$border-color: rgba(204, 204, 204, 0.33);
$url-color: rgba(102, 102, 102, .8);
$url-active-color: #ccc

View File

@@ -1,63 +0,0 @@
.login-background {
display: flex;
height: 100vh;
width: 100vw;
justify-content: center;
align-items: center;
background-color: #B3E5FC;
}
.login-box {
display: flex;
width: 800px;
height: 450px;
background-color: #448AFF;
}
.login-box-left {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
flex: 2;
}
.login-box-left-text {
font-size: 3rem;
color: white;
font-weight: bold;
}
.login-box-left-text>div:last-child {
font-size: 1.8rem;
font-weight: normal;
}
.login-box-right {
position: relative;
height: 100%;
flex: 3;
background-color: white;
}
.login-from-text {
position: absolute;
top: 60px;
left: 40px;
font-weight: bold;
font-size: 2rem;
}
.login-from {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 30px;
width: 100%;
height: 100%;
}
.login-from-item {
width: 80%;
}

View File

@@ -1,35 +0,0 @@
.top-bar {
display: flex;
justify-content: right;
background-color: #317ece;
padding: 10px 20px;
}
.top-bar > button:hover {
color: #F5F5F5;
border-color: #F5F5F5;
}
.search-row {
display: flex;
gap: 20px;
margin: 10px 10px;
}
.search-row > * {
display: flex;
gap: 5px;
align-items: center;
flex: 1;
font-size: 1rem;
}
.search-row > *:not(.operation-buttons) > *:last-child {
flex: 1;
}
.operation-buttons {
display: flex;
justify-content: right;
gap: 10px;
}

View File

@@ -0,0 +1,164 @@
@use "@/assets/css/mixins" as mixins;
@use "@/assets/css/constants" as constants;
.nav {
display: flex;
position: fixed;
align-items: center;
z-index: 1;
width: 100%;
height: 70px;
background-color: white;
border: {
bottom: {
width: 1px;
style: solid;
color: constants.$border-color;
}
}
animation: .5s ease both;
@include mixins.unique-keyframes {
0% {
transform: translateY(-100%);
}
100% {
transform: translateY(0);
}
}
.logo {
padding: 0 40px;
.title {
font-size: 2.8em;
font-family: century gothic, texgyreadventor, stheiti, sans-serif;
}
}
nav {
display: flex;
justify-content: end;
flex: 1;
transition: {
property: all;
duration: .5s;
};
.menu {
padding: 0 30px;
.item {
display: inline-block;
font-size: 1.5em;
transition: {
property: all;
duration: .3s;
};
a {
padding: 5px 20px;
color: constants.$url-color;
}
}
.active {
border: {
bottom: {
width: 2px;
style: solid;
color: constants.$url-active-color;
};
};
}
:hover {
transform: translateY(-5px);
}
}
.dropdown-menu-button {
display: none;
margin: 0 20px;
width: 45px;
height: 45px;
justify-content: center;
align-items: center;
border-radius: 6px;
}
.dropdown-menu-button.active {
background-color: transparentize(constants.$focus-color, 0.8);
border: {
width: 1px;
color: constants.$focus-color;
style: solid;
};
}
@media screen and (max-width: 900px) {
.menu {
display: none;
}
.dropdown-menu-button {
display: flex;
}
}
}
}
.dropdown-menu-content {
display: none;
@media screen and (max-width: 900px) {
display: none;
position: fixed;
width: 100%;
top: 70px;
border: {
width: 1px;
color: constants.$border-color;
style: solid;
};
background-color: white;
z-index: 1;
ul {
li {
font-size: 1.2em;
text-align: center;
a {
display: block;
width: 100%;
height: 100%;
padding: 10px;
}
:hover {
background-color: constants.$focus-color;
}
}
}
}
}
@media screen and (max-width: 900px){
.dropdown-menu-content.show {
display: block;
}
}
.nav.hide {
animation: .5s ease both;
@include mixins.unique-keyframes {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-100%);
}
}
}

View File

@@ -0,0 +1,63 @@
.login-background {
display: flex;
height: 100vh;
width: 100vw;
justify-content: center;
align-items: center;
background-color: #B3E5FC;
}
.login-box {
display: flex;
width: 800px;
height: 450px;
background-color: #448AFF;
}
.login-box-left {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
flex: 2;
}
.login-box-left-text {
font-size: 3rem;
color: white;
font-weight: bold;
> div:last-child {
font-size: 1.8rem;
font-weight: normal;
}
}
.login-box-right {
position: relative;
height: 100%;
flex: 3;
background-color: white;
}
.login-from-text {
position: absolute;
top: 60px;
left: 40px;
font-weight: bold;
font-size: 2rem;
}
.login-from {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 30px;
width: 100%;
height: 100%;
}
.login-from-item {
width: 80%;
}

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

@@ -0,0 +1 @@
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path d="M958.577476 311.180252c0 15.286148-5.878894 29.522384-16.564257 40.166815L551.623951 741.043556c-1.39272 1.425466-2.577708 2.537799-3.846608 3.449565l-2.905166 2.304486c-11.416004 11.362792-24.848944 16.945951-39.068807 16.945951-14.475689 0-28.010961-5.708002-38.110993-16.056698L79.475588 355.310332c-10.467399-10.613732-16.236799-24.786523-16.236799-39.893592 0-14.772448 5.599532-28.726252 15.753799-39.286772 10.18599-10.497075 24.390503-16.539698 38.95215-16.539698 14.382569 0 28.009937 5.723352 38.366819 16.142655l350.169241 353.968777 359.428116-358.485651c10.30981-10.248412 23.781636-15.909341 37.994336-15.909341 14.889105 0 28.859281 6.05081 39.333844 16.999163C953.126324 282.725176 958.577476 296.556183 958.577476 311.180252z" /></svg>

After

Width:  |  Height:  |  Size: 862 B

View File

@@ -0,0 +1 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" version="1.1" width="32" height="32"><path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z" /></svg>

After

Width:  |  Height:  |  Size: 687 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="24" height="24" viewBox="0 0 24 24"><g style="mix-blend-mode:passthrough"><g style="mix-blend-mode:passthrough"><path d="M11.2633,0.229798C11.6966,-0.0765992,12.3034,-0.0765992,12.7367,0.229798C12.7367,0.229798,23.5367,7.86616,23.5367,7.86616C23.829,8.07284,24,8.39063,24,8.72727C24,8.72727,24,20.7273,24,20.7273C24,21.5953,23.6207,22.4277,22.9456,23.0414C22.2704,23.6552,21.3548,24,20.4,24C20.4,24,3.6,24,3.6,24C2.64522,24,1.72955,23.6552,1.05442,23.0414C0.379284,22.4277,0,21.5953,0,20.7273C0,20.7273,0,8.72727,0,8.72727C0,8.39063,0.170968,8.07284,0.463271,7.86616C0.463271,7.86616,11.2633,0.229798,11.2633,0.229798C11.2633,0.229798,11.2633,0.229798,11.2633,0.229798ZM9.6,21.8182C9.6,21.8182,14.4,21.8182,14.4,21.8182C14.4,21.8182,14.4,13.0909,14.4,13.0909C14.4,13.0909,9.6,13.0909,9.6,13.0909C9.6,13.0909,9.6,21.8182,9.6,21.8182C9.6,21.8182,9.6,21.8182,9.6,21.8182ZM16.8,21.8182C16.8,21.8182,16.8,12,16.8,12C16.8,11.3975,16.2628,10.9091,15.6,10.9091C15.6,10.9091,8.4,10.9091,8.4,10.9091C7.73726,10.9091,7.2,11.3975,7.2,12C7.2,12,7.2,21.8182,7.2,21.8182C7.2,21.8182,3.6,21.8182,3.6,21.8182C3.28174,21.8182,2.97652,21.7032,2.75147,21.4987C2.52643,21.2941,2.4,21.0166,2.4,20.7273C2.4,20.7273,2.4,9.26081,2.4,9.26081C2.4,9.26081,12,2.47294,12,2.47294C12,2.47294,21.6,9.26081,21.6,9.26081C21.6,9.26081,21.6,20.7273,21.6,20.7273C21.6,21.0166,21.4735,21.2941,21.2485,21.4987C21.0235,21.7032,20.7182,21.8182,20.4,21.8182C20.4,21.8182,16.8,21.8182,16.8,21.8182C16.8,21.8182,16.8,21.8182,16.8,21.8182Z" fill-rule="evenodd" fill-opacity="1"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1 @@
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M487.1 425c-1.4-11.2-19-23.1-28.2-31.9-5.1-5-29-23.1-30.4-29.9-1.4-6.6 9.7-21.5 13.3-28.9 5.1-10.7 8.8-23.7 11.3-32.6 18.8-66.1 20.7-156.9-6.2-211.2-10.2-20.6-38.6-49-56.4-62.5-42-31.7-119.6-35.3-170.1-16.6-14.1 5.2-27.8 9.8-40.1 17.1-33.1 19.4-68.3 32.5-78.1 71.6-24.2 10.8-31.5 41.8-30.3 77.8.2 7 4.1 15.8 2.7 22.4-.7 3.3-5.2 7.6-6.1 9.8-11.6 27.7-2.3 64 11.1 83.7 8.1 11.9 21.5 22.4 39.2 25.2.7 10.6 3.3 19.7 8.2 30.4 3.1 6.8 14.7 19 10.4 27.7-2.2 4.4-21 13.8-27.3 17.6C89 407.2 73.7 415 54.2 429c-12.6 9-32.3 10.2-29.2 31.1 2.1 14.1 10.1 31.6 14.7 45.8.7 2 1.4 4.1 2.1 6h422c4.9-15.3 9.7-30.9 14.6-47.2 3.4-11.4 10.2-27.8 8.7-39.7zM205.9 33.7c1.8-.5 3.4.7 4.9 2.4-.2 5.2-5.4 5.1-8.9 6.8-5.4 6.7-13.4 9.8-20 17.2-6.8 7.5-14.4 27.7-23.4 30-4.5 1.1-9.7-.8-13.6-.5-10.4.7-17.7 6-28.3 7.5 13.6-29.9 56.1-54 89.3-63.4zm-104.8 93.6c13.5-14.9 32.1-24.1 54.8-25.9 11.7 29.7-8.4 65-.9 97.6 2.3 9.9 10.2 25.4-2.4 25.7.3-28.3-34.8-46.3-61.3-29.6-1.8-21.5-4.9-51.7 9.8-67.8zm36.7 200.2c-1-4.1-2.7-12.9-2.3-15.1 1.6-8.7 17.1-12.5 11-24.7-11.3-.1-13.8 10.2-24.1 11.3-26.7 2.6-45.6-35.4-44.4-58.4 1-19.5 17.6-38.2 40.1-35.8 16 1.8 21.4 19.2 24.5 34.7 9.2.5 22.5-.4 26.9-7.6-.6-17.5-8.8-31.6-8.2-47.7 1-30.3 17.5-57.6 4.8-87.4 13.6-30.9 53.5-55.3 83.1-70 36.6-18.3 94.9-3.7 129.3 15.8 19.7 11.1 34.4 32.7 48.3 50.7-19.5-5.8-36.1 4.2-33.1 20.3 16.3-14.9 44.2-.2 52.5 16.4 7.9 15.8 7.8 39.3 9 62.8 2.9 57-10.4 115.9-39.1 157.1-7.7 11-14.1 23-24.9 30.6-26 18.2-65.4 34.7-99.2 23.4-44.7-15-65-44.8-89.5-78.8.7 18.7 13.8 34.1 26.8 48.4 11.3 12.5 25 26.6 39.7 32.4-12.3-2.9-31.1-3.8-36.2 7.2-28.6-1.9-55.1-4.8-68.7-24.2-10.6-15.4-21.4-41.4-26.3-61.4zm222 124.1c4.1-3 11.1-2.9 17.4-3.6-5.4-2.7-13-3.7-19.3-2.2-.1-4.2-2-6.8-3.2-10.2 10.6-3.8 35.5-28.5 49.6-20.3 6.7 3.9 9.5 26.2 10.1 37 .4 9-.8 18-4.5 22.8-18.8-.6-35.8-2.8-50.7-7 .9-6.1-1-12.1.6-16.5zm-17.2-20c-16.8.8-26-1.2-38.3-10.8.2-.8 1.4-.5 1.5-1.4 18 8 40.8-3.3 59-4.9-7.9 5.1-14.6 11.6-22.2 17.1zm-12.1 33.2c-1.6-9.4-3.5-12-2.8-20.2 25-16.6 29.7 28.6 2.8 20.2zM226 438.6c-11.6-.7-48.1-14-38.5-23.7 9.4 6.5 27.5 4.9 41.3 7.3.8 4.4-2.8 10.2-2.8 16.4zM57.7 497.1c-4.3-12.7-9.2-25.1-14.8-36.9 30.8-23.8 65.3-48.9 102.2-63.5 2.8-1.1 23.2 25.4 26.2 27.6 16.5 11.7 37 21 56.2 30.2 1.2 8.8 3.9 20.2 8.7 35.5.7 2.3 1.4 4.7 2.2 7.2H57.7zm240.6 5.7h-.8c.3-.2.5-.4.8-.5v.5zm7.5-5.7c2.1-1.4 4.3-2.8 6.4-4.3 1.1 1.4 2.2 2.8 3.2 4.3h-9.6zm15.1-24.7c-10.8 7.3-20.6 18.3-33.3 25.2-6 3.3-27 11.7-33.4 10.2-3.6-.8-3.9-5.3-5.4-9.5-3.1-9-10.1-23.4-10.8-37-.8-17.2-2.5-46 16-42.4 14.9 2.9 32.3 9.7 43.9 16.1 7.1 3.9 11.1 8.6 21.9 9.5-.1 1.4-.1 2.8-.2 4.3-5.9 3.9-15.3 3.8-21.8 7.1 9.5.4 17 2.7 23.5 5.9-.1 3.4-.3 7-.4 10.6zm53.4 24.7h-14c-.1-3.2-2.8-5.8-6.1-5.8s-5.9 2.6-6.1 5.8h-17.4c-2.8-4.4-5.7-8.6-8.9-12.5 2.1-2.2 4-4.7 6-6.9 9 3.7 14.8-4.9 21.7-4.2 7.9.8 14.2 11.7 25.4 11l-.6 12.6zm8.7 0c.2-4 .4-7.8.6-11.5 15.6-7.3 29 1.3 35.7 11.5H383zm83.4-37c-2.3 11.2-5.8 24-9.9 37.1-.2-.1-.4-.1-.6-.1H428c.6-1.1 1.2-2.2 1.9-3.3-2.6-6.1-9-8.7-10.9-15.5 12.1-22.7 6.5-93.4-24.2-78.5 4.3-6.3 15.6-11.5 20.8-19.3 13 10.4 20.8 20.3 33.2 31.4 6.8 6 20 13.3 21.4 23.1.8 5.5-2.6 18.9-3.8 25.1zM222.2 130.5c5.4-14.9 27.2-34.7 45-32 7.7 1.2 18 8.2 12.2 17.7-30.2-7-45.2 12.6-54.4 33.1-8.1-2-4.9-13.1-2.8-18.8zm184.1 63.1c8.2-3.6 22.4-.7 29.6-5.3-4.2-11.5-10.3-21.4-9.3-37.7.5 0 1 0 1.4.1 6.8 14.2 12.7 29.2 21.4 41.7-5.7 13.5-43.6 25.4-43.1 1.2zm20.4-43zm-117.2 45.7c-6.8-10.9-19-32.5-14.5-45.3 6.5 11.9 8.6 24.4 17.8 33.3 4.1 4 12.2 9 8.2 20.2-.9 2.7-7.8 8.6-11.7 9.7-14.4 4.3-47.9.9-36.6-17.1 11.9.7 27.9 7.8 36.8-.8zm27.3 70c3.8 6.6 1.4 18.7 12.1 20.6 20.2 3.4 43.6-12.3 58.1-17.8 9-15.2-.8-20.7-8.9-30.5-16.6-20-38.8-44.8-38-74.7 6.7-4.9 7.3 7.4 8.2 9.7 8.7 20.3 30.4 46.2 46.3 63.5 3.9 4.3 10.3 8.4 11 11.2 2.1 8.2-5.4 18-4.5 23.5-21.7 13.9-45.8 29.1-81.4 25.6-7.4-6.7-10.3-21.4-2.9-31.1zm-201.3-9.2c-6.8-3.9-8.4-21-16.4-21.4-11.4-.7-9.3 22.2-9.3 35.5-7.8-7.1-9.2-29.1-3.5-40.3-6.6-3.2-9.5 3.6-13.1 5.9 4.7-34.1 49.8-15.8 42.3 20.3zm299.6 28.8c-10.1 19.2-24.4 40.4-54 41-.6-6.2-1.1-15.6 0-19.4 22.7-2.2 36.6-13.7 54-21.6zm-141.9 12.4c18.9 9.9 53.6 11 79.3 10.2 1.4 5.6 1.3 12.6 1.4 19.4-33 1.8-72-6.4-80.7-29.6zm92.2 46.7c-1.7 4.3-5.3 9.3-9.8 11.1-12.1 4.9-45.6 8.7-62.4-.3-10.7-5.7-17.5-18.5-23.4-26-2.8-3.6-16.9-12.9-.2-12.9 13.1 32.7 58 29 95.8 28.1z"/></svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1 @@
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 0 0-94.3-139.9 437.71 437.71 0 0 0-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3 0.1 19.9-16 36-35.9 36z" /></svg>

After

Width:  |  Height:  |  Size: 439 B

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

@@ -0,0 +1 @@
<svg id="logo" xmlns="http://www.w3.org/2000/svg" width="911.75" height="898.91" viewBox="0 0 911.75 898.91"><path d="M828.52,831.72c-216,259.7-774.46,115.85-765.17-319.5-19-292.76,370.82-602.09,704.81-367.11,44.53,48.2,81.39,32.52,121.28,46.67,28.28,14.3,21.36,42.61,52.36,142.6,31.6,101.09,60.92,133.92-17.45,182.4-28,11.34-46.6-2.34-117.82-25.31-92-29.65-109-22.33-132.66-48-27.26-29.62-14.42-50.13-41.31-80.13C507.2,259.81,336.39,351,290.74,489.72c-7,26.59-20.37,92.15,8.72,162.38,25.72,62.11,71.11,95.23,99.93,115.7C554.85,878.2,781.33,840.41,828.52,831.72Zm50.11-584.95c-15.4,5.93.73,47,16,40.82C910.07,281.66,893.94,240.63,878.63,246.77Zm-152,180.06c-4.34,15.92,35.73,26.64,39.92,10.68C770.9,421.58,730.83,410.86,726.63,426.83Z" transform="translate(-62.68 -62.16)"/></svg>

After

Width:  |  Height:  |  Size: 780 B

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

@@ -0,0 +1 @@
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><path d="M950.784 768v72.704q0 15.36-10.24 25.6t-26.624 11.264H110.08q-15.36 0-25.6-11.264t-11.264-25.6V768q0-15.36 11.264-25.6t25.6-11.264h803.84q15.36 0 26.624 11.264t10.24 25.6z m0-292.864v73.728q0 14.336-10.24 25.6t-26.624 10.24H110.08q-15.36 0-25.6-10.24t-11.264-25.6v-73.728q0-14.336 11.264-25.6t25.6-10.24h803.84q15.36 0 26.624 10.24t10.24 25.6z m0-291.84V256q0 14.336-10.24 25.6t-26.624 11.264H110.08q-15.36 0-25.6-11.264T73.216 256V183.296q0-15.36 11.264-26.624t25.6-10.24h803.84q15.36 0 26.624 10.24t10.24 26.624z" /></svg>

After

Width:  |  Height:  |  Size: 649 B

View File

@@ -0,0 +1,23 @@
import React from 'react'
import '@/assets/css/components/common/fit-center.scss'
interface FitCenterProps
extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
vertical?: boolean
}
const FitCenter: React.FC<FitCenterProps> = (props) => {
const { className, vertical, ..._props } = props
return (
<>
<div
className={`fit-center${className ? ' ' + className : ''}${
vertical ? ' flex-vertical' : ' flex-horizontal'
}`}
{..._props}
></div>
</>
)
}
export default FitCenter

View File

@@ -0,0 +1,30 @@
import React from 'react'
import '@/assets/css/components/common/fit-fullscreen.scss'
interface FitFullscreenProps
extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
zIndex?: number
backgroundColor?: string
}
const FitFullScreen = forwardRef<HTMLDivElement, FitFullscreenProps>((props, ref) => {
const { zIndex, backgroundColor, className, style, ..._props } = props
return (
<>
<div
className={`fit-fullscreen${className ? ' ' + className : ''}`}
style={{
zIndex,
backgroundColor,
...style
}}
ref={ref}
{..._props}
>
{props.children}
</div>
</>
)
})
export default FitFullScreen

View File

@@ -0,0 +1,541 @@
import React from 'react'
import '@/assets/css/components/common/hide-scrollbar.scss'
interface HideScrollbarProps
extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
isPreventScroll?: boolean
isPreventVerticalScroll?: boolean
isPreventHorizontalScroll?: boolean
isShowVerticalScrollbar?: boolean
isHiddenVerticalScrollbarWhenFull?: boolean
isShowHorizontalScrollbar?: boolean
isHiddenHorizontalScrollbarWhenFull?: boolean
}
export interface HideScrollbarElement {
scrollTo(x: number, y: number, smooth?: boolean): void
scrollX(x: number, smooth?: boolean): void
scrollY(y: number, smooth?: boolean): void
scrollLeft(length: number, smooth?: boolean): void
scrollRight(length: number, smooth?: boolean): void
scrollUp(length: number, smooth?: boolean): void
scrollDown(length: number, smooth?: boolean): void
getX(): number
getY(): number
addEventListenerWithType<K extends keyof HTMLElementEventMap>(
type: K,
listener: (this: HTMLDivElement, ev: HTMLElementEventMap[K]) => never,
options?: boolean | AddEventListenerOptions
): void
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions
): void
removeEventListenerWithType<K extends keyof HTMLElementEventMap>(
type: K,
listener: (this: HTMLDivElement, ev: HTMLElementEventMap[K]) => never,
options?: boolean | EventListenerOptions
): void
removeEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | EventListenerOptions
): void
}
const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>((props, ref) => {
useImperativeHandle<HideScrollbarElement, HideScrollbarElement>(
ref,
() => {
return {
scrollTo(x, y, smooth?: boolean) {
rootRef.current?.scrollTo({
left: x,
top: y,
behavior: smooth === false ? 'instant' : 'smooth'
})
},
scrollX(x, smooth?: boolean) {
rootRef.current?.scrollTo({
left: x,
behavior: smooth === false ? 'instant' : 'smooth'
})
},
scrollY(y, smooth?: boolean) {
rootRef.current?.scrollTo({
top: y,
behavior: smooth === false ? 'instant' : 'smooth'
})
},
scrollLeft(length, smooth?: boolean) {
rootRef.current?.scrollTo({
left: rootRef.current?.scrollLeft - length,
behavior: smooth === false ? 'instant' : 'smooth'
})
},
scrollRight(length, smooth?: boolean) {
rootRef.current?.scrollTo({
left: rootRef.current?.scrollLeft + length,
behavior: smooth === false ? 'instant' : 'smooth'
})
},
scrollUp(length, smooth?: boolean) {
rootRef.current?.scrollTo({
top: rootRef.current?.scrollTop - length,
behavior: smooth === false ? 'instant' : 'smooth'
})
},
scrollDown(length, smooth?: boolean) {
rootRef.current?.scrollTo({
top: rootRef.current?.scrollTop + length,
behavior: smooth === false ? 'instant' : 'smooth'
})
},
getX() {
return rootRef.current?.scrollLeft ?? 0
},
getY() {
return rootRef.current?.scrollTop ?? 0
},
addEventListenerWithType<K extends keyof HTMLElementEventMap>(
type: K,
listener: (this: HTMLDivElement, ev: HTMLElementEventMap[K]) => never,
options?: boolean | AddEventListenerOptions
): void {
rootRef.current?.addEventListener<K>(type, listener, options)
},
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions
): void {
rootRef.current?.addEventListener(type, listener, options)
},
removeEventListenerWithType<K extends keyof HTMLElementEventMap>(
type: K,
listener: (this: HTMLDivElement, ev: HTMLElementEventMap[K]) => never,
options?: boolean | EventListenerOptions
): void {
rootRef.current?.removeEventListener<K>(type, listener, options)
},
removeEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | EventListenerOptions
): void {
rootRef.current?.removeEventListener(type, listener, options)
}
}
},
[]
)
const maskRef = useRef<HTMLDivElement>(null)
const rootRef = useRef<HTMLDivElement>(null)
const contentRef = useRef<HTMLDivElement>(null)
const wheelListenerRef = useRef<(event: WheelEvent) => void>(() => undefined)
const lastScrollbarClickPositionRef = useRef({ x: -1, y: -1 })
const lastScrollbarTouchPositionRef = useRef({ x: -1, y: -1 })
const lastTouchPositionRef = useRef({ x: -1, y: -1 })
const [verticalScrollbarWidth, setVerticalScrollbarWidth] = useState(0)
const [verticalScrollbarLength, setVerticalScrollbarLength] = useState(100)
const [verticalScrollbarPosition, setVerticalScrollbarPosition] = useState(0)
const [verticalScrollbarOnClick, setVerticalScrollbarOnClick] = useState(false)
const [verticalScrollbarOnTouch, setVerticalScrollbarOnTouch] = useState(false)
const [horizontalScrollbarWidth, setHorizontalScrollbarWidth] = useState(0)
const [horizontalScrollbarLength, setHorizontalScrollbarLength] = useState(100)
const [horizontalScrollbarPosition, setHorizontalScrollbarPosition] = useState(0)
const [horizontalScrollbarOnClick, setHorizontalScrollbarOnClick] = useState(false)
const [horizontalScrollbarOnTouch, setHorizontalScrollbarOnTouch] = useState(false)
const {
isPreventScroll,
isPreventVerticalScroll,
isPreventHorizontalScroll,
isShowVerticalScrollbar,
isHiddenVerticalScrollbarWhenFull,
isShowHorizontalScrollbar,
isHiddenHorizontalScrollbarWhenFull,
..._props
} = props
const isPreventAnyScroll =
isPreventScroll || isPreventVerticalScroll || isPreventHorizontalScroll
const handleDefaultTouchStart = useCallback(
(event: React.TouchEvent) => {
if (event.touches.length !== 1 || isPreventScroll) {
lastTouchPositionRef.current = { x: -1, y: -1 }
return
}
const { clientX, clientY } = event.touches[0]
lastTouchPositionRef.current = { x: clientX, y: clientY }
},
[isPreventScroll]
)
const handleDefaultTouchmove = useCallback(
(event: React.TouchEvent) => {
if (event.touches.length !== 1 || isPreventScroll) {
lastTouchPositionRef.current = { x: -1, y: -1 }
return
}
const { clientX, clientY } = event.touches[0]
if (!isPreventVerticalScroll) {
rootRef.current?.scrollTo({
top: rootRef.current?.scrollTop + (lastTouchPositionRef.current.y - clientY),
behavior: 'instant'
})
}
if (!isPreventHorizontalScroll) {
rootRef.current?.scrollTo({
left: rootRef.current?.scrollLeft + (lastTouchPositionRef.current.x - clientX),
behavior: 'instant'
})
}
lastTouchPositionRef.current = { x: clientX, y: clientY }
},
[isPreventHorizontalScroll, isPreventScroll, isPreventVerticalScroll]
)
const handleDefaultMouseDown = (event: React.MouseEvent) => {
if (isPreventAnyScroll)
if (event.button === 1) {
event.preventDefault()
}
}
const handleDefaultKeyDown = useCallback(
(event: React.KeyboardEvent) => {
if (
isPreventScroll &&
[
'ArrowUp',
'ArrowDown',
'ArrowLeft',
'ArrowRight',
' ',
'',
'PageUp',
'PageDown',
'Home',
'End'
].find((value) => value === event.key)
) {
event.preventDefault()
}
if (
isPreventVerticalScroll &&
['ArrowUp', 'ArrowDown', ' ', '', 'PageUp', 'PageDown', 'Home', 'End'].find(
(value) => value === event.key
)
) {
event.preventDefault()
}
if (
isPreventHorizontalScroll &&
['ArrowLeft', 'ArrowRight'].find((value) => value === event.key)
) {
event.preventDefault()
}
},
[isPreventHorizontalScroll, isPreventScroll, isPreventVerticalScroll]
)
const handleScrollbarMouseEvent = (eventFlag: string, scrollbarFlag: string) => {
return (event: React.MouseEvent) => {
switch (eventFlag) {
case 'down':
lastScrollbarClickPositionRef.current = { x: event.clientX, y: event.clientY }
switch (scrollbarFlag) {
case 'vertical':
setVerticalScrollbarOnClick(true)
break
case 'horizontal':
setHorizontalScrollbarOnClick(true)
break
}
break
case 'up':
case 'leave':
setVerticalScrollbarOnClick(false)
setHorizontalScrollbarOnClick(false)
break
case 'move':
if (verticalScrollbarOnClick) {
rootRef.current?.scrollTo({
top:
rootRef.current?.scrollTop +
((event.clientY - lastScrollbarClickPositionRef.current.y) /
(rootRef.current?.clientHeight ?? 1)) *
(contentRef.current?.clientHeight ?? 0),
behavior: 'instant'
})
}
if (horizontalScrollbarOnClick) {
rootRef.current?.scrollTo({
left:
rootRef.current?.scrollLeft +
((event.clientX - lastScrollbarClickPositionRef.current.x) /
(rootRef.current?.clientWidth ?? 1)) *
(contentRef.current?.clientWidth ?? 0),
behavior: 'instant'
})
}
lastScrollbarClickPositionRef.current = {
x: event.clientX,
y: event.clientY
}
}
}
}
const handleScrollbarTouchEvent = (eventFlag: string, scrollbarFlag: string) => {
return (event: React.TouchEvent) => {
switch (eventFlag) {
case 'start':
if (event.touches.length !== 1) {
return
}
lastScrollbarTouchPositionRef.current = {
x: event.touches[0].clientX,
y: event.touches[0].clientY
}
switch (scrollbarFlag) {
case 'vertical':
setVerticalScrollbarOnTouch(true)
break
case 'horizontal':
setHorizontalScrollbarOnTouch(true)
break
}
break
case 'end':
case 'cancel':
setVerticalScrollbarOnTouch(false)
setHorizontalScrollbarOnTouch(false)
break
case 'move':
if (event.touches.length !== 1) {
return
}
if (verticalScrollbarOnTouch) {
rootRef.current?.scrollTo({
top:
rootRef.current?.scrollTop +
((event.touches[0].clientY -
lastScrollbarClickPositionRef.current.y) /
(rootRef.current?.clientHeight ?? 1)) *
(contentRef.current?.clientHeight ?? 0),
behavior: 'instant'
})
}
if (horizontalScrollbarOnTouch) {
rootRef.current?.scrollTo({
left:
rootRef.current?.scrollLeft +
((event.touches[0].clientX -
lastScrollbarClickPositionRef.current.x) /
(rootRef.current?.clientWidth ?? 1)) *
(contentRef.current?.clientWidth ?? 0),
behavior: 'instant'
})
}
lastScrollbarClickPositionRef.current = {
x: event.touches[0].clientX,
y: event.touches[0].clientY
}
}
}
}
const handleDefaultScroll = () => {
setVerticalScrollbarPosition(
((rootRef.current?.scrollTop ?? 0) / (contentRef.current?.clientHeight ?? 1)) * 100
)
setHorizontalScrollbarPosition(
((rootRef.current?.scrollLeft ?? 0) / (contentRef.current?.clientWidth ?? 1)) * 100
)
}
useEffect(() => {
const windowResizeListener = () => {
setVerticalScrollbarWidth(
(rootRef.current?.offsetWidth ?? 0) - (rootRef.current?.clientWidth ?? 0)
)
setHorizontalScrollbarWidth(
(rootRef.current?.offsetHeight ?? 0) - (rootRef.current?.clientHeight ?? 0)
)
rootRef.current &&
setVerticalScrollbarLength(
(rootRef.current.clientHeight / (contentRef.current?.clientHeight ?? 0)) * 100
)
rootRef.current &&
setHorizontalScrollbarLength(
(rootRef.current.clientWidth / (contentRef.current?.clientWidth ?? 0)) * 100
)
return windowResizeListener
}
setTimeout(() => {
windowResizeListener()
}, 1000)
window.addEventListener('resize', windowResizeListener())
rootRef.current?.removeEventListener('wheel', wheelListenerRef.current)
if (isPreventAnyScroll) {
const handleDefaultWheel = (event: WheelEvent) => {
if (!event.altKey && !event.ctrlKey) {
if (isPreventScroll) {
event.preventDefault()
return
}
if (isPreventVerticalScroll && !event.shiftKey && !event.deltaX) {
event.preventDefault()
return
}
if (isPreventHorizontalScroll && (event.shiftKey || !event.deltaY)) {
event.preventDefault()
return
}
}
}
wheelListenerRef.current = handleDefaultWheel
rootRef.current?.addEventListener('wheel', handleDefaultWheel, { passive: false })
}
rootRef.current &&
setVerticalScrollbarLength(
(rootRef.current.clientHeight / (contentRef.current?.clientHeight ?? 0)) * 100
)
rootRef.current &&
setHorizontalScrollbarLength(
(rootRef.current.clientWidth / (contentRef.current?.clientWidth ?? 0)) * 100
)
return () => {
window.removeEventListener('resize', windowResizeListener)
}
}, [
horizontalScrollbarLength,
isPreventAnyScroll,
isPreventHorizontalScroll,
isPreventScroll,
isPreventVerticalScroll
])
return (
<>
<div
className={'hide-scrollbar-mask'}
ref={maskRef}
onMouseMove={
!isPreventScroll ? handleScrollbarMouseEvent('move', 'all') : undefined
}
onTouchMove={
!isPreventScroll ? handleScrollbarTouchEvent('move', 'all') : undefined
}
onMouseUp={!isPreventScroll ? handleScrollbarMouseEvent('up', 'all') : undefined}
onTouchEnd={!isPreventScroll ? handleScrollbarTouchEvent('end', 'all') : undefined}
onMouseLeave={
!isPreventScroll ? handleScrollbarMouseEvent('leave', 'all') : undefined
}
onTouchCancel={
!isPreventScroll ? handleScrollbarTouchEvent('cancel', 'all') : undefined
}
>
<div
ref={rootRef}
className={'hide-scrollbar-selection'}
tabIndex={0}
style={{
width: `calc(100vw + ${verticalScrollbarWidth}px)`,
height: `calc(100vh + ${horizontalScrollbarWidth}px)`,
touchAction: isPreventAnyScroll ? 'none' : '',
msTouchAction: isPreventAnyScroll ? 'none' : ''
}}
{..._props}
onMouseDown={isPreventAnyScroll ? handleDefaultMouseDown : undefined}
onKeyDown={isPreventAnyScroll ? handleDefaultKeyDown : undefined}
onTouchStart={isPreventAnyScroll ? handleDefaultTouchStart : undefined}
onTouchMove={isPreventAnyScroll ? handleDefaultTouchmove : undefined}
onScroll={handleDefaultScroll}
>
<div className={'hide-scrollbar-content'} ref={contentRef}>
{props.children}
</div>
</div>
<div
hidden={
!isShowVerticalScrollbar ||
((isHiddenVerticalScrollbarWhenFull ?? true) &&
verticalScrollbarLength === 100)
}
className={'scrollbar vertical-scrollbar'}
>
<div className={'box'}>
<div
className={'block'}
style={{
height: `${verticalScrollbarLength}%`,
top: `clamp(0px, ${verticalScrollbarPosition}%, ${
100 - verticalScrollbarLength
}%)`
}}
onMouseDown={
!isPreventScroll && !isPreventVerticalScroll
? handleScrollbarMouseEvent('down', 'vertical')
: undefined
}
onTouchStart={
!isPreventScroll && !isPreventVerticalScroll
? handleScrollbarTouchEvent('start', 'vertical')
: undefined
}
/>
</div>
</div>
<div
hidden={
!isShowHorizontalScrollbar ||
((isHiddenHorizontalScrollbarWhenFull ?? true) &&
horizontalScrollbarLength === 100)
}
className={'scrollbar horizontal-scrollbar'}
>
<div className={'box'}>
<div
className={'block'}
style={{
width: `${horizontalScrollbarLength}%`,
left: `clamp(0px, ${horizontalScrollbarPosition}%, ${
100 - horizontalScrollbarLength
}%)`
}}
onMouseDown={
!isPreventScroll && !isPreventHorizontalScroll
? handleScrollbarMouseEvent('down', 'horizontal')
: undefined
}
onTouchStart={
!isPreventScroll && !isPreventHorizontalScroll
? handleScrollbarTouchEvent('start', 'horizontal')
: undefined
}
/>
</div>
</div>
</div>
</>
)
})
export default HideScrollbar

View File

@@ -0,0 +1,39 @@
import React from 'react'
import _ from 'lodash'
import '@/assets/css/components/common/indicator.scss'
interface IndicatorProps {
total: number
current: number
onSwitch?: (index: number) => void
}
const Indicator: React.FC<IndicatorProps> = (props) => {
const { total, current, onSwitch } = props
const handleClick = (index: number) => {
return () => {
onSwitch && onSwitch(index)
}
}
return (
<>
<ul className={'dot-list flex-vertical'}>
{_.range(0, total).map((_value, index) => {
return (
<li
key={index}
className={'item center-box' + (index === current ? ' active' : '')}
onClick={handleClick(index)}
>
<div className={'dot'} />
</li>
)
})}
</ul>
</>
)
}
export default Indicator

View File

@@ -0,0 +1,28 @@
import React from 'react'
import Icon from '@ant-design/icons'
import '@/assets/css/components/common/loading-mask.scss'
import FitFullScreen from '@/components/common/FitFullScreen'
import { COLOR_FONT_MAIN } from '@/constants/Common.constants'
const LoadingMask: React.FC = () => {
const loadingIcon = (
<>
<Icon
component={IconFatwebLoading}
style={{ fontSize: 24, color: COLOR_FONT_MAIN }}
spin
/>
</>
)
return (
<>
<FitFullScreen>
<div className={'loading-mask'}>
<AntdSpin indicator={loadingIcon} />
</div>
</FitFullScreen>
</>
)
}
export default LoadingMask

View File

@@ -0,0 +1,30 @@
import React from 'react'
import FitCenter from '@/components/common/FitCenter.tsx'
import Icon from '@ant-design/icons'
import '@/assets/css/components/home/footer.scss'
import FitFullScreen from '@/components/common/FitFullScreen.tsx'
import { NavLink } from 'react-router-dom'
const Footer: React.FC = () => {
return (
<>
<FitFullScreen backgroundColor={'#333'}>
<FitCenter vertical={true} style={{ gap: '20px' }}>
<div className={'icons'}>
<NavLink to={'https://github.com/FatttSnake'}>
<Icon component={IconFatwebGithub} className={'icon'} />
</NavLink>
<NavLink to={'https://ci.fatweb.top'}>
<Icon component={IconFatwebJenkins} className={'icon'} />
</NavLink>
</div>
<div className={'links'}>
<NavLink to={'mailto:fatttsnake@fatweb.top'}>Mail</NavLink>
</div>
</FitCenter>
</FitFullScreen>
</>
)
}
export default Footer

View File

@@ -0,0 +1,17 @@
import React from 'react'
import FitCenter from '@/components/common/FitCenter.tsx'
const OxygenToolbox: React.FC = () => {
return (
<>
<FitCenter vertical>
<div>
<div style={{ fontSize: '4.5em', fontWeight: 'bold' }}>Oxygen Toolbox</div>
<div style={{ fontSize: '1.4em', textAlign: 'end' }}>is coming soon...</div>
</div>
</FitCenter>
</>
)
}
export default OxygenToolbox

View File

@@ -0,0 +1,50 @@
import React from 'react'
import Icon from '@ant-design/icons'
import '@/assets/css/components/home/slogan.scss'
import FitCenter from '@/components/common/FitCenter.tsx'
interface SloganProps {
onClickScrollDown: (event: React.MouseEvent) => void
}
const Slogan: React.FC<SloganProps> = (props) => {
const [slogan, setSlogan] = useState('')
const [sloganType, setSloganType] = useState(true)
const typeText = '因为热爱 所以折腾'
if (sloganType) {
setTimeout(() => {
if (slogan.length < typeText.length) {
setSlogan(typeText.substring(0, slogan.length + 1))
} else {
setSloganType(false)
}
}, 250)
} else {
setTimeout(() => {
if (slogan.length > 0) {
setSlogan(typeText.substring(0, slogan.length - 1))
} else {
setSloganType(true)
}
}, 100)
}
return (
<>
<FitCenter>
<div className={'center-box'}>
<div className={'big-logo'}>FatWeb</div>
<span id={'slogan'} className={'slogan'}>
/* {slogan || <>&nbsp;</>} <span className={'cursor'}>|</span> */
</span>
</div>
<div className={'scroll-down'} onClick={props.onClickScrollDown}>
<Icon component={IconFatwebDown} style={{ fontSize: '1.8em', color: '#666' }} />
</div>
</FitCenter>
</>
)
}
export default Slogan

View File

@@ -0,0 +1,173 @@
import React from 'react'
import '@/assets/css/components/home/home.scss'
import FitFullScreen from '@/components/common/FitFullScreen'
import { MainFrameworkContext } from '@/pages/MainFramework'
import Slogan from '@/components/home/Slogan'
import OxygenToolbox from '@/components/home/OxygenToolbox'
import Indicator from '@/components/common/Indicator.tsx'
import Footer from '@/components/home/Footer'
const Home: React.FC = () => {
const {
hideScrollbarRef,
navbarHiddenState: { navbarHidden, setNavbarHidden },
showDropdownMenuState: { setShowDropdownMenu },
preventScrollState: { setPreventScroll }
} = useContext(MainFrameworkContext)
const fitFullScreenRef = useRef<HTMLDivElement>(null)
const scrollTimeout = useRef(0)
const clickStart = useRef(0)
const [currentContent, setCurrentContent] = useState(0)
useEffect(() => {
setTimeout(() => {
setNavbarHidden(true)
setPreventScroll(true)
})
}, [setNavbarHidden, setPreventScroll])
useLayoutEffect(() => {
const handleWindowResize = () => {
handleScrollToContent(currentContent)()
}
window.addEventListener('resize', handleWindowResize)
return () => window.removeEventListener('resize', handleWindowResize)
})
const handleScrollToContent = (index: number) => {
return () => {
setShowDropdownMenu(false)
if (!index) {
setNavbarHidden(true)
hideScrollbarRef.current?.scrollY(0)
} else {
hideScrollbarRef.current?.scrollY(
(fitFullScreenRef.current?.offsetHeight ?? 0) * index
)
setNavbarHidden(false)
}
}
}
const handleScrollUp = () => {
if (currentContent <= 0) {
return
}
handleScrollToContent(currentContent - 1)()
clearTimeout(scrollTimeout.current)
scrollTimeout.current = setTimeout(() => {
setCurrentContent(currentContent - 1)
}, 500)
}
const handleScrollDown = () => {
if (currentContent >= content.length - 1) {
return
}
handleScrollToContent(currentContent + 1)()
clearTimeout(scrollTimeout.current)
scrollTimeout.current = setTimeout(() => {
setCurrentContent(currentContent + 1)
}, 500)
}
const handleWheel = (event: React.WheelEvent) => {
if (event.altKey || event.ctrlKey || event.shiftKey || event.deltaY === 0) {
return
}
if (event.deltaY > 0) {
handleScrollDown()
} else {
handleScrollUp()
}
}
const handleTouchStart = (event: React.TouchEvent) => {
clickStart.current = event.touches[0].clientY
}
const handleTouchEnd = (event: React.TouchEvent) => {
const moveLength = clickStart.current - event.changedTouches[0].clientY
if (Math.abs(moveLength) < 100) {
return
}
if (moveLength > 0) {
handleScrollDown()
} else {
handleScrollUp()
}
}
const handleMouseDown = (event: React.MouseEvent) => {
clickStart.current = event.clientY
}
const handleMouseUp = (event: React.MouseEvent) => {
const moveLength = clickStart.current - event.clientY
if (Math.abs(moveLength) < 100) {
return
}
if (moveLength > 0) {
handleScrollDown()
} else {
handleScrollUp()
}
}
const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === 'ArrowUp') {
handleScrollUp()
}
if (event.key === 'ArrowDown') {
handleScrollDown()
}
}
const handleIndicatorSwitch = (index: number) => {
setCurrentContent(index)
handleScrollToContent(index)()
}
const content = [
{
backgroundColor: '#FBFBFB',
ref: fitFullScreenRef,
children: <Slogan onClickScrollDown={handleScrollDown} />
},
{ children: <OxygenToolbox /> },
{ children: <Footer /> }
]
return (
<>
<div
tabIndex={0}
onWheel={handleWheel}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onKeyDown={handleKeyDown}
>
{content.map((element, index) => {
return <FitFullScreen key={index} {...element} />
})}
</div>
<div hidden={navbarHidden} className={'indicator'}>
<Indicator
total={content.length}
current={currentContent}
onSwitch={handleIndicatorSwitch}
/>
</div>
</>
)
}
export default Home

View File

@@ -1,4 +1,4 @@
const PRODUCTION_NAME = 'Pinnacle OA'
const PRODUCTION_NAME = 'FatWeb'
const TOKEN_NAME = 'JWT_TOKEN'
const COLOR_PRODUCTION = '#00D4FF'
const COLOR_BACKGROUND = '#F5F5F5'

View File

@@ -1,69 +0,0 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@@ -1,9 +1,9 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import '@/assets/css/base.css'
import '@/assets/css/common.css'
import zh_CN from 'antd/locale/zh_CN'
import '@/assets/css/base.scss'
import '@/assets/css/common.scss'
import App from './App.tsx'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>

View File

@@ -1,11 +0,0 @@
import React from 'react'
const Home: React.FC = () => {
return (
<>
<h1>FatWeb</h1>
</>
)
}
export default Home

View File

@@ -2,7 +2,7 @@ import React from 'react'
import { login } from '@/utils/auth.ts'
import { LOGIN_SUCCESS, LOGIN_USERNAME_PASSWORD_ERROR } from '@/constants/Common.constants.ts'
import { setToken } from '@/utils/common.ts'
import '@/assets/css/login.css'
import '@/assets/css/pages/login.scss'
const Login: React.FC = () => {
const [messageApi, contextHolder] = message.useMessage()

181
src/pages/MainFramework.tsx Normal file
View File

@@ -0,0 +1,181 @@
import React from 'react'
import '@/assets/css/pages/header.scss'
import router from '@/router'
import LoadingMask from '@/components/common/LoadingMask'
import HideScrollbar, { HideScrollbarElement } from '@/components/common/HideScrollbar'
import Icon from '@ant-design/icons'
import { COLOR_FONT_SECONDARY } from '@/constants/Common.constants.ts'
export const MainFrameworkContext = createContext<{
navbarHiddenState: {
navbarHidden: boolean
setNavbarHidden: (newValue: boolean) => void
}
preventScrollState: {
preventScroll: boolean
setPreventScroll: (newValue: boolean) => void
}
showVerticalScrollbarState: {
showVerticalScrollbar: boolean
setShowVerticalScrollbar: (newValue: boolean) => void
}
showHorizontalScrollbarState: {
showHorizontalScrollbar: boolean
setShowHorizontalScrollbar: (newValue: boolean) => void
}
showDropdownMenuState: {
showDropdownMenu: boolean
setShowDropdownMenu: (newValue: boolean) => void
}
hideScrollbarRef: React.RefObject<HideScrollbarElement>
}>({
navbarHiddenState: {
navbarHidden: false,
setNavbarHidden: () => undefined
},
preventScrollState: {
preventScroll: false,
setPreventScroll: () => undefined
},
showVerticalScrollbarState: {
showVerticalScrollbar: false,
setShowVerticalScrollbar: () => undefined
},
showHorizontalScrollbarState: {
showHorizontalScrollbar: false,
setShowHorizontalScrollbar: () => undefined
},
showDropdownMenuState: {
showDropdownMenu: false,
setShowDropdownMenu: () => undefined
},
hideScrollbarRef: createRef()
})
const MainFramework: React.FC = () => {
const routeId = useMatches()[1].id
const routeChildren = router.routes[0].children?.find((value) => value.id === routeId)?.children
const pathname = useLocation().pathname
const hideScrollbarRef = useRef<HideScrollbarElement>(null)
const [navbarHidden, setNavbarHidden] = useState(true)
const [preventScroll, setPreventScroll] = useState(false)
const [showVerticalScrollbar, setShowVerticalScrollbar] = useState(false)
const [showHorizontalScrollbar, setShowHorizontalScrollbar] = useState(false)
const [showDropdownMenu, setShowDropdownMenu] = useState(false)
useEffect(() => {
setNavbarHidden(false)
}, [pathname])
const handleMenuDropdownButtonClick = () => {
setShowDropdownMenu(!showDropdownMenu)
}
return (
<>
<HideScrollbar
ref={hideScrollbarRef}
isPreventVerticalScroll={preventScroll}
isShowHorizontalScrollbar={true}
>
<div className={'body'}>
<div>
<header className={'nav' + (navbarHidden ? ' hide' : '')}>
<a className={'logo'} href={'https://fatweb.top'}>
<span className={'title'}>FatWeb</span>
</a>
<nav>
<ul className={'menu'}>
{routeChildren?.map((route) => {
return (
<li className={'item'} key={route.id}>
<NavLink
to={route.path ?? ''}
className={({ isActive, isPending }) =>
isPending
? 'pending'
: isActive
? 'active'
: ''
}
>
{(route.handle as RouteHandle).name}
</NavLink>
</li>
)
})}
</ul>
<div
className={
'dropdown-menu-button' + (showDropdownMenu ? ' active' : '')
}
>
<Icon
component={IconFatwebMenu}
style={{ fontSize: '2.6em', color: COLOR_FONT_SECONDARY }}
onClick={handleMenuDropdownButtonClick}
/>
</div>
</nav>
</header>
<div
className={'dropdown-menu-content' + (showDropdownMenu ? ' show' : '')}
>
<ul>
{routeChildren?.map((route) => {
return (
<li className={'item'} key={route.id}>
<NavLink
to={route.path ?? ''}
className={({ isActive, isPending }) =>
isPending ? 'pending' : isActive ? 'active' : ''
}
>
{(route.handle as RouteHandle).name}
</NavLink>
</li>
)
})}
</ul>
</div>
</div>
<MainFrameworkContext.Provider
value={{
navbarHiddenState: { navbarHidden, setNavbarHidden },
preventScrollState: { preventScroll, setPreventScroll },
showVerticalScrollbarState: {
showVerticalScrollbar,
setShowVerticalScrollbar
},
showHorizontalScrollbarState: {
showHorizontalScrollbar,
setShowHorizontalScrollbar
},
showDropdownMenuState: {
showDropdownMenu,
setShowDropdownMenu
},
hideScrollbarRef
}}
>
<Suspense
fallback={
<>
<LoadingMask />
</>
}
>
<Outlet />
</Suspense>
</MainFrameworkContext.Provider>
</div>
</HideScrollbar>
</>
)
}
export default MainFramework

View File

@@ -10,20 +10,36 @@ const routes: RouteObject[] = [
id: 'login',
Component: React.lazy(() => import('@/pages/Login'))
},
{
path: '/loading',
id: 'loading',
Component: React.lazy(() => import('@/components/common/LoadingMask'))
},
{
path: '',
id: 'manager',
Component: React.lazy(() => import('@/pages/Home')),
id: 'mainFramework',
Component: React.lazy(() => import('@/pages/MainFramework')),
children: [
{
id: 'manager-sub',
path: 'sub',
Component: React.lazy(() => import('@/pages/Home'))
path: '',
id: 'home',
Component: React.lazy(() => import('@/components/home')),
handle: {
name: '主页',
menu: true,
auth: false
}
},
{
path: 'https://blog.fatweb.top',
id: 'blog',
handle: {
name: '博客',
menu: true,
auth: false
}
}
],
handle: {
auth: false
}
]
},
{
path: '*',

14
src/vite-env.d.ts vendored
View File

@@ -1,12 +1,9 @@
/// <reference types="vite/client" />
type Captcha = {
value: string
base64Src: string
}
type RouteHandle = {
auth: boolean
name?: string
menu?: boolean
auth?: boolean
}
type _Response<T> = {
@@ -15,6 +12,11 @@ type _Response<T> = {
data: T | null
}
type Captcha = {
value: string
base64Src: string
}
type Token = {
token: string
}

View File

@@ -26,6 +26,7 @@ export default defineConfig({
'react-router',
'react-router-dom',
{
react: ['Suspense', 'createContext'],
'react-router': ['useMatches', 'RouterProvider'],
'react-router-dom': ['createBrowserRouter'],
antd: ['message']