Compare commits

72 Commits

Author SHA1 Message Date
395c547e85 Fix(HideScrollbar): Fix error stylesheet 2024-07-15 10:04:06 +08:00
43779227c2 Fix(main): Fix error path of icon 2024-07-15 00:02:27 +08:00
69e572ae23 Build(Builder): Update config 2024-06-24 17:28:13 +08:00
df98339838 Build(Builder): Update config 2024-06-14 18:00:24 +08:00
7d3ec39da3 Fix(Form): Fix ref error 2024-06-13 15:17:31 +08:00
32686848d2 Build(package.json): Upgrade dependencies 2024-06-13 15:16:18 +08:00
1df704f65e Build(.env): Update env 2024-06-13 15:15:26 +08:00
c5637d5e7f Refactor(Icon): Update icon.png 2024-05-21 18:16:09 +08:00
a7577373ef Refactor(Protocol): Optimize desktop and app protocol 2024-05-21 17:41:57 +08:00
3dc434a6ac Refactor(Compiler): Fix the bug that unable to initialize compiler 2024-05-21 16:01:40 +08:00
898075cf6e Refactor(Playground): Fix the bug that unexpectedly remove styles for base
Fix the bug that unexpectedly remove styles for base when editing code
2024-05-21 16:00:31 +08:00
58bb5eb262 Fix URL exception after build 2024-05-21 14:39:09 +08:00
31a458416d Refactor(URL): Optimize url 2024-05-21 14:38:28 +08:00
2b1bd719ad Build(Package): Optimize clean script 2024-05-21 14:37:10 +08:00
606064b1fb Fix(Navigation): Fix navigate bug 2024-05-20 23:00:09 +08:00
19113a56af Build(Icon): Update icon 2024-05-19 17:20:59 +08:00
6dd1f51c89 Refactor(LocalPage): Remove favorite attribute
Remove favorite attribute from LocalCard
2024-05-19 17:20:24 +08:00
22e2e15db8 Fix(Edit): Fix undefined in AntdSelect 2024-05-12 05:20:19 +08:00
4f55364f65 Refactor(Render): Allow to read and write clipboard in iframe 2024-05-12 04:09:06 +08:00
d0ddb276c0 Fix(Sidebar): Fix the bug that submenu can not display 2024-05-11 18:58:09 +08:00
8f22084d10 Refactor(StoreCard): Optimize android qr-code url 2024-05-11 16:37:31 +08:00
30ec21a71e Feat(Tool): Support install tool
Support install tool and execute locally
2024-05-08 17:22:20 +08:00
513f66418a Refactor(Service): Add network error message 2024-05-08 17:17:21 +08:00
580249a85c Refactor(StoreCard): Fix the bug that open android emulator automatically
Fix the bug that open android emulator automatically when click mask
2024-05-08 15:10:12 +08:00
1ef2db5f47 Feat(Electron-store): Add electron store
Add electron store to support install tool
2024-05-08 12:40:56 +08:00
d677aeed07 Refactor(Window): Automatically show application
Automatically show application when opening the tool from a browser
2024-05-08 09:41:28 +08:00
361a5c923d Refactor(Variable): Optimize variable name 2024-05-06 16:02:26 +08:00
c87e697ea6 Refactor(TS): Rename some file from *.tsx to *.ts 2024-05-06 15:25:32 +08:00
058984ad85 Build(SensitiveWord): Fix tsc error 2024-05-06 15:18:06 +08:00
3ddd4c88e0 Build(Package): Upgrade dependencies 2024-05-06 11:11:09 +08:00
c64777e759 Build(Package): Upgrade dependencies 2024-05-02 01:46:49 +08:00
d918e527a0 Feat(StoreCard): Support emulator
Support run tool by emulator
2024-05-01 18:39:31 +08:00
b51fdb11f2 Feat(Menu): Support multi-platform
Support add multi-platform tool to menu
2024-05-01 18:18:17 +08:00
6e2012cd7a Fix(ToolExecute and ToolView): Fix the bug that can not load tool
Fix the bug that can not load tool when switch tool
2024-05-01 18:07:42 +08:00
cab0c9d879 Fix(ToolBase and ToolTemplate): Optimize panel side 2024-05-01 18:03:23 +08:00
a5ca0be53c Refactor(Modal): Optimize modal code 2024-05-01 16:15:44 +08:00
e5bc568d23 Refactor(StoreCard): Remove tooltip
Remove tooltip from author info
2024-05-01 14:56:24 +08:00
2176c0ce2a Fix submenu style exception. Optimize menu stylesheet 2024-05-01 14:55:45 +08:00
ac2e3c8e6e Feat(Menu): Add drop mask
Add drop mask to menu. Optimize tool description.
2024-05-01 14:46:50 +08:00
63c76440a6 Refactor(Card): Optimize import 2024-05-01 14:40:51 +08:00
7f034db314 Refactor(Sign): Optimize navigate to login 2024-05-01 14:39:34 +08:00
d326c8b8e5 Refactor(Tool): Optimize use experience
Remove navigate delay. Clean out old code before setting up compile code.
2024-05-01 14:35:44 +08:00
ea68945df1 Feat(Menu): Add tool menu via drag and drop
Drag and drop a tool card to add tool menu
2024-05-01 14:30:26 +08:00
59eef73895 Fix(Tool): Fix the bug that can not load when switch between two tools
Fix the bug that can not load when switch between two tools. Optimize navigate when can not find tool.
2024-05-01 13:41:27 +08:00
2f9c01981b Refactor(UrlCard): Optimize UrlCard 2024-05-01 12:39:05 +08:00
5250b662f5 Feat(ToolRepository): Add favorite tool list
Add favorite tool list to user tool repository
2024-04-30 23:55:06 +08:00
77166831bf Refactor(Card): Component all cards
Make all cards into components
2024-04-30 17:57:04 +08:00
9152ec8dfb Refactor(URL): Change URL_TOOL_FAVORITE 2024-04-30 17:38:49 +08:00
cfddc85302 Feat(PersonalTool): Add pagination
Add pagination to personal tool management
2024-04-30 17:36:39 +08:00
76470d741f Fix(CreateTool): Fix input validation
Fix can not submit because of input validation bug
2024-04-30 17:31:34 +08:00
62a70f92ae Feat(Store): Add tool favorite control
Add tool favorite control
2024-04-30 17:30:43 +08:00
5a12020d69 Refactor(Navigate): Optimize navigate
Unified management navigation
2024-04-30 17:25:24 +08:00
71faca682e Chore(Eslint): Dismiss exhaustive deps warning
Add config to dismiss exhaustive deps warning
2024-04-30 17:11:43 +08:00
cd1802b5d6 Fix(FileSelector): Fix scrollbar can not auto hide bug
Fix scrollbar can not auto hide bug. Optimize scrollbar style.
2024-04-30 17:11:05 +08:00
0aad1ac966 Refactor(HideScrollbar): Add scrollbar padding attributes
Add scrollbar padding attributes to allow custom scrollbar padding style
2024-04-30 17:10:17 +08:00
e209bea2f3 Refactor(Router): Change home to repository
Change home route to repository. Switch repository and store place in menu.
2024-04-30 17:08:19 +08:00
c65c548ecc Refactor(Editor): Add a prompt to leave the page
Add a prompt to leave the page when content has been changed
2024-04-30 17:03:06 +08:00
9b96c7dd62 Refactor(Form): Optimize form input experience
Add placeholder. Optimize form validation. Hide ID input.
2024-04-30 16:59:46 +08:00
a2d0afb38c Perf(Statistics): Optimize chart display performance for online users
Optimize the display performance of online user icons when selecting for a long time
2024-04-30 16:30:59 +08:00
48c9ec148a Refactor(Statistics): Correct incorrect spelling
Correct WEAK to WEEK
2024-04-30 16:28:50 +08:00
07602bebc8 Fix(Input): Fixed the problem of not validating whitespace characters
Fixed the problem of not validating whitespace characters
2024-04-30 16:27:20 +08:00
5c2a77db30 Optimize: User Management - optimize nickname verification 2024-03-26 11:42:36 +08:00
7d5265ecda Feat: create - support simulated Android device preview 2024-03-22 17:41:10 +08:00
058746702d Feat: edit and view - support simulated Android device preview 2024-03-22 16:54:59 +08:00
84d2e6c3f8 Fix: create - can not upgrade tool bug 2024-03-21 16:04:59 +08:00
61d3bb21ad Feat: store - support multiple platforms 2024-03-19 18:34:45 +08:00
8730513340 Feat: all - support URL protocol 2024-03-19 16:54:36 +08:00
6f53a867c3 Feat: all - support multiple platforms 2024-03-18 17:23:45 +08:00
c098368a79 Style: all - change page padding to 20px 2024-03-15 15:29:52 +08:00
de3d1831d0 Optimize: tsconfig - optimize alias config 2024-03-15 15:29:33 +08:00
715806b85d Optimize: editor - switch to local monaco editor loader 2024-03-14 16:59:27 +08:00
0b314e4ea9 Optimize: compiler - switch to local esbuild.wasm 2024-03-14 16:58:08 +08:00
156 changed files with 7526 additions and 4602 deletions

View File

@@ -1,9 +0,0 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

View File

@@ -1,3 +1,7 @@
VITE_API_URL=http://localhost:8080 VITE_PLATFORM=DESKTOP
VITE_DESKTOP_PROTOCOL=dev-oxygen-desktop
VITE_APP_PROTOCOL=dev-oxygen-app
VITE_UI_URL=${DEV_UI_URL}
VITE_API_URL=${DEV_API_URL}
VITE_API_TOKEN_URL=${VITE_API_URL}/token VITE_API_TOKEN_URL=${VITE_API_URL}/token
VITE_TURNSTILE_SITE_KEY=${TURNSTILE_SITE_KEY} VITE_TURNSTILE_SITE_KEY=${TURNSTILE_SITE_KEY}

View File

@@ -1,3 +1,7 @@
VITE_PLATFORM=DESKTOP
VITE_DESKTOP_PROTOCOL=oxygen-desktop
VITE_APP_PROTOCOL=oxygen-app
VITE_UI_URL=${PRODUCT_UI_URL}
VITE_API_URL=${PRODUCT_API_URL} VITE_API_URL=${PRODUCT_API_URL}
VITE_API_TOKEN_URL=${VITE_API_URL}/token VITE_API_TOKEN_URL=${VITE_API_URL}/token
VITE_TURNSTILE_SITE_KEY=${TURNSTILE_SITE_KEY} VITE_TURNSTILE_SITE_KEY=${TURNSTILE_SITE_KEY}

View File

@@ -1,4 +1,8 @@
NODE_ENV=development NODE_ENV=development
VITE_PLATFORM=DESKTOP
VITE_DESKTOP_PROTOCOL=test-oxygen-desktop
VITE_APP_PROTOCOL=test-oxygen-app
VITE_UI_URL=${TEST_UI_URL}
VITE_API_URL=${TEST_API_URL} VITE_API_URL=${TEST_API_URL}
VITE_API_TOKEN_URL=${VITE_API_URL}/token VITE_API_TOKEN_URL=${VITE_API_URL}/token
VITE_TURNSTILE_SITE_KEY=${TURNSTILE_SITE_KEY} VITE_TURNSTILE_SITE_KEY=${TURNSTILE_SITE_KEY}

View File

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

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 978 B

After

Width:  |  Height:  |  Size: 978 B

View File

@@ -149,7 +149,10 @@ const matchComponents: IMatcher[] = [
pattern: /^Mentions/, pattern: /^Mentions/,
styleDir: 'mentions' styleDir: 'mentions'
}, },
{
pattern: /^QRCode/,
styleDir: 'qr-code'
},
{ {
pattern: /^Step/, pattern: /^Step/,
styleDir: 'steps' styleDir: 'steps'
@@ -337,6 +340,7 @@ const primitiveNames = [
'Rate', 'Rate',
'Result', 'Result',
'Row', 'Row',
'QRCode',
'Select', 'Select',
'SelectOptGroup', 'SelectOptGroup',
'SelectOption', 'SelectOption',
@@ -417,7 +421,6 @@ export const AntDesignResolver = (options: AntDesignResolverOptions = {}): Compo
sideEffects: getSideEffects(importName, options) sideEffects: getSideEffects(importName, options)
} }
} }
return undefined return undefined
} }
} }

View File

@@ -1,5 +1,7 @@
appId: com.electron.app productName: OxygenDesktop
productName: oxygen-desktop copyright: Copyright © 2017-2024 FatWeb. All rights reserved.
appId: top.fatweb.oxygen.desktop
artifactName: ${name}-${version}-${os}-${arch}.${ext}
directories: directories:
buildResources: build buildResources: build
files: files:
@@ -8,35 +10,78 @@ files:
- '!electron.vite.config.{js,ts,mjs,cjs}' - '!electron.vite.config.{js,ts,mjs,cjs}'
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' - '!{tsconfig.json,tsconfig.node.json}'
asarUnpack: asarUnpack:
- resources/** - resources/**
win: win:
executableName: oxygen-desktop target:
- target: nsis
arch:
- x64
- ia32
- arm64
- target: msi
arch:
- x64
- ia32
- arm64
- target: zip
arch:
- x64
- ia32
- arm64
nsis: nsis:
artifactName: ${name}-${version}-setup.${ext} oneClick: false
shortcutName: ${productName} allowToChangeInstallationDirectory: true
uninstallDisplayName: ${productName} artifactName: ${name}-${version}-${os}-${arch}-setup.${ext}
createDesktopShortcut: always shortcutName: Oxygen Desktop
createDesktopShortcut: true
msi:
oneClick: false
shortcutName: Oxygen Desktop
mac: mac:
category: public.app-category.productivity
target:
- default
entitlementsInherit: build/entitlements.mac.plist entitlementsInherit: build/entitlements.mac.plist
darkModeSupport: true
extendInfo: extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera. NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone. NSMicrophoneUsageDescription: Application requests access to the device's microphone.
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
notarize: false notarize: false
dmg:
artifactName: ${name}-${version}.${ext}
linux: linux:
target: target:
- AppImage - target: AppImage
- snap arch:
- deb - x64
maintainer: electronjs.org - arm64
- armv7l
- target: deb
arch:
- x64
- arm64
- armv7l
- target: rpm
arch:
- x64
- arm64
- armv7l
- target: pacman
arch:
- x64
- arm64
- armv7l
- target: tar.gz
arch:
- x64
- arm64
- armv7l
category: Utility category: Utility
appImage: desktop:
artifactName: ${name}-${version}.${ext} Name: Oxygen Desktop
Comment: Oxygen Toolbox multi-platform desktop version
npmRebuild: false npmRebuild: false
publish: publish:
provider: generic provider: generic

View File

@@ -1,4 +1,4 @@
import { resolve } from 'path' import { fileURLToPath, URL } from 'node:url'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite' import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import AutoImport from 'unplugin-auto-import/vite' import AutoImport from 'unplugin-auto-import/vite'
@@ -10,7 +10,7 @@ import { FileSystemIconLoader } from 'unplugin-icons/loaders'
export default defineConfig({ export default defineConfig({
main: { main: {
plugins: [externalizeDepsPlugin()] plugins: [externalizeDepsPlugin({ exclude: ['electron-store'] })]
}, },
preload: { preload: {
plugins: [externalizeDepsPlugin()] plugins: [externalizeDepsPlugin()]
@@ -82,7 +82,7 @@ export default defineConfig({
], ],
resolve: { resolve: {
alias: { alias: {
'@': resolve('src/renderer/src') '@': fileURLToPath(new URL('./src/renderer/src', import.meta.url))
} }
} }
} }

5471
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,87 +1,101 @@
{ {
"name": "oxygen-desktop", "name": "oxygen-desktop",
"version": "1.0.0", "private": true,
"description": "An Electron application with React and TypeScript", "version": "1.0.0-SNAPSHOT",
"description": "Oxygen Toolbox multi-platform desktop version",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "example.com", "author": {
"homepage": "https://electron-vite.org", "name": "FatttSnake",
"email": "fatttsnake@fatweb.top",
"url": "https://fatweb.top"
},
"homepage": "https://tool.fatweb.top",
"scripts": { "scripts": {
"dev": "electron-vite dev", "dev": "electron-vite dev --sourcemap --remote-debugging-port=9000",
"format": "prettier --write .", "format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", "typecheck": "tsc",
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false", "clean": "rimraf out dist .eslintrc-auto-import.json src/renderer/auto-imports.d.ts",
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"start": "electron-vite preview", "start": "electron-vite preview",
"build": "electron-vite build && npm run typecheck", "build": "electron-vite build && npm run typecheck",
"build-test": "electron-vite build --mode testing && npm run typecheck",
"postinstall": "electron-builder install-app-deps", "postinstall": "electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir", "build:unpack": "npm run build && electron-builder --dir",
"build-test:unpack": "npm run build-test && electron-builder --dir",
"build:win": "npm run build && electron-builder --win", "build:win": "npm run build && electron-builder --win",
"build:mac": "electron-vite build && electron-builder --mac", "build-test:win": "npm run build-test && electron-builder --win",
"build:linux": "electron-vite build && electron-builder --linux" "build:mac": "npm run build && electron-builder --mac",
"build-test:mac": "npm run build-test && electron-builder --mac",
"build:linux": "npm run build && electron-builder --linux",
"build-test:linux": "npm run build-test && electron-builder --linux"
}, },
"dependencies": { "dependencies": {
"@electron-toolkit/preload": "^3.0.0", "@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0", "@electron-toolkit/utils": "^3.0.0",
"electron-updater": "^6.1.7" "electron-store": "^9.0.0",
"electron-updater": "^6.2.1"
}, },
"devDependencies": { "devDependencies": {
"@ant-design/icons": "^5.3.1", "@ant-design/icons": "^5.3.7",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@electron-toolkit/eslint-config-prettier": "^2.0.0", "@electron-toolkit/eslint-config-prettier": "^2.0.0",
"@electron-toolkit/eslint-config-ts": "^1.0.1", "@electron-toolkit/eslint-config-ts": "^2.0.0",
"@electron-toolkit/tsconfig": "^1.0.1", "@electron-toolkit/tsconfig": "^1.0.1",
"@marsidev/react-turnstile": "^0.5.3", "@marsidev/react-turnstile": "^0.7.1",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@svgr/core": "^8.1.0", "@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0",
"@types/jsdom": "^21.1.6", "@types/jsdom": "^21.1.7",
"@types/lodash": "^4.17.0", "@types/lodash": "^4.17.5",
"@types/node": "^18.19.9", "@types/node": "^20.14.2",
"@types/react": "^18.2.48", "@types/react": "^18.3.3",
"@types/react-dom": "^18.2.18", "@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/eslint-plugin": "^7.13.0",
"@typescript-eslint/parser": "^6.21.0", "@typescript-eslint/parser": "^7.13.0",
"@typescript/ata": "^0.9.4", "@typescript/ata": "^0.9.6",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.3.1",
"antd": "^5.15.2", "antd": "^5.18.1",
"axios": "^1.6.7", "axios": "^1.7.2",
"dayjs": "^1.11.10", "custom-protocol-check": "^1.4.0",
"dayjs": "^1.11.11",
"echarts": "^5.5.0", "echarts": "^5.5.0",
"electron": "^28.2.0", "electron": "^31.0.1",
"electron-builder": "^24.9.1", "electron-builder": "^24.13.3",
"electron-vite": "^2.0.0", "electron-vite": "^2.2.0",
"esbuild-wasm": "^0.20.1", "esbuild-wasm": "^0.21.5",
"eslint": "^8.56.0", "eslint": "^8.57.0",
"eslint-config-love": "^52.0.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-config-standard-with-typescript": "^43.0.1",
"eslint-plugin-import": "^2.29.1", "eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-promise": "^6.1.1", "eslint-plugin-promise": "^6.2.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.5", "eslint-plugin-react-refresh": "^0.4.7",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"fflate": "^0.8.2", "fflate": "^0.8.2",
"jsdom": "^24.0.0", "jsdom": "^24.1.0",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"match-sorter": "^6.3.4", "match-sorter": "^6.3.4",
"moment": "^2.30.1", "moment": "^2.30.1",
"monaco-editor": "^0.47.0", "monaco-editor": "^0.49.0",
"monaco-jsx-syntax-highlight": "^1.2.0", "monaco-jsx-syntax-highlight": "^1.2.0",
"prettier": "^3.2.4", "prettier": "^3.3.2",
"react": "^18.2.0", "react": "^18.3.1",
"react-dom": "^18.2.0", "react-dom": "^18.3.1",
"react-draggable": "^4.4.6", "react-draggable": "^4.4.6",
"react-router": "^6.22.3", "react-router": "^6.23.1",
"react-router-dom": "^6.22.3", "react-router-dom": "^6.23.1",
"sass": "^1.71.1", "sass": "^1.77.5",
"size-sensor": "^1.0.2", "size-sensor": "^1.0.2",
"stylelint-config-prettier": "^9.0.5", "stylelint-config-prettier": "^9.0.5",
"typescript": "^5.3.3", "typescript": "^5.4.5",
"unplugin-auto-import": "^0.17.5", "unplugin-auto-import": "^0.17.6",
"unplugin-icons": "^0.18.5", "unplugin-icons": "^0.19.0",
"vanilla-tilt": "^1.8.1", "vanilla-tilt": "^1.8.1",
"vite": "^5.0.12" "vite": "^5.2.13"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

21
src/main/dataProcess.ts Normal file
View File

@@ -0,0 +1,21 @@
import Store, { Schema } from 'electron-store'
import { ipcMain } from 'electron'
const schema: Schema<StoreSchema> = {
installedTools: {
default: []
}
}
const store = new Store<StoreSchema>({ schema })
ipcMain.handle('store:installTool', (_, value: Record<string, Record<Platform, ToolVo>>) => {
const installedTools = store.get('installedTools')
store.set('installedTools', { ...installedTools, ...value })
return store.get('installedTools')
})
ipcMain.handle('store:getInstalledTool', () => {
return store.get('installedTools')
})

93
src/main/global.d.ts vendored Normal file
View File

@@ -0,0 +1,93 @@
type Platform = 'WEB' | 'DESKTOP' | 'ANDROID'
interface ImportMetaEnv {
readonly VITE_PLATFORM: Platform
readonly VITE_DESKTOP_PROTOCOL: string
readonly VITE_APP_PROTOCOL: string
readonly VITE_UI_URL: string
readonly VITE_API_URL: string
readonly VITE_API_TOKEN_URL: string
readonly VITE_TURNSTILE_SITE_KEY: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
interface StoreSchema {
installedTools: Record<string, Record<Platform, ToolVo>>
}
interface ToolVo {
id: string
name: string
toolId: string
icon: string
platform: Platform
description: string
base: ToolBaseVo
author: UserWithInfoVo
ver: string
keywords: string[]
categories: ToolCategoryVo[]
source: ToolDataVo
dist: ToolDataVo
entryPoint: string
publish: string
review: 'NONE' | 'PROCESSING' | 'PASS' | 'REJECT'
createTime: string
updateTime: string
favorite: boolean
}
interface ToolBaseVo {
id: string
name: string
source: ToolDataVo
dist: ToolDataVo
platform: Platform
compiled: boolean
createTime: string
updateTime: string
}
interface ToolDataVo {
id: string
data?: string
createTime?: string
updateTime?: string
}
interface UserWithInfoVo {
id: string
username: string
twoFactor: boolean
verified: boolean
locking: boolean
expiration: string
credentialsExpiration: string
enable: boolean
currentLoginTime: string
currentLoginIp: string
lastLoginTime: string
lastLoginIp: string
createTime: string
updateTime: string
userInfo: UserInfoVo
}
interface UserInfoVo {
id: string
userId: string
nickname: string
avatar: string
email: string
}
interface ToolCategoryVo {
id: string
name: string
enable: boolean
createTime: string
updateTime: string
}

View File

@@ -1,11 +1,70 @@
import { app, shell, BrowserWindow, ipcMain } from 'electron' import { app, shell, BrowserWindow, ipcMain, protocol, net } from 'electron'
import { join } from 'path' import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils' import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/logo.ico?asset' import icon from '../../build/icon.ico?asset'
import path from 'node:path'
import url from 'node:url'
function createWindow(): void { let mainWindow: BrowserWindow
// Application singleton execution
if (!app.requestSingleInstanceLock()) {
app.quit()
}
// Register protocol client
const args: string[] = []
if (!app.isPackaged) {
args.push(path.resolve(process.argv[1]))
}
args.push('--')
app.setAsDefaultProtocolClient(import.meta.env.VITE_DESKTOP_PROTOCOL, process.execPath, args)
// app.removeAsDefaultProtocolClient(import.meta.env.VITE_DESKTOP_PROTOCOL, process.execPath, args)
const handleArgv = (argv: string[]) => {
const prefix = `${import.meta.env.VITE_DESKTOP_PROTOCOL}:`
const offset = app.isPackaged ? 1 : 2
const url = argv.find((arg, index) => index >= offset && arg.startsWith(prefix))
if (url) {
handleUrl(url)
}
}
const handleUrl = (url: string) => {
const { hostname, pathname } = new URL(url)
if (hostname === 'openurl' && mainWindow) {
mainWindow.webContents.send('open-url', pathname)
mainWindow.show()
}
}
// Windows
handleArgv(process.argv)
app.on('second-instance', (_, argv) => {
if (process.platform === 'win32') {
handleArgv(argv)
}
})
// macOS
app.on('open-url', (_, argv) => {
handleUrl(argv)
})
protocol.registerSchemesAsPrivileged([
{
scheme: 'local',
privileges: {
standard: true,
secure: true,
supportFetchAPI: true
}
}
])
const createWindow = () => {
// Create the browser window. // Create the browser window.
const mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: 900, width: 900,
height: 670, height: 670,
show: false, show: false,
@@ -32,7 +91,10 @@ function createWindow(): void {
if (is.dev && process.env['ELECTRON_RENDERER_URL']) { if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
void mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) void mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else { } else {
void mainWindow.loadFile(join(__dirname, '../renderer/index.html')) // void mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
void mainWindow.loadURL(
`local://oxygen.fatweb.top/${join(__dirname, '../renderer/index.html')}`
)
} }
} }
@@ -40,6 +102,17 @@ function createWindow(): void {
// initialization and is ready to create browser windows. // initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs. // Some APIs can only be used after this event occurs.
void app.whenReady().then(() => { void app.whenReady().then(() => {
protocol.handle('local', (request) => {
const { host } = new URL(request.url)
if (host === 'oxygen.fatweb.top') {
const filePath = request.url.slice('local://oxygen.fatweb.top/'.length)
return net.fetch(url.pathToFileURL(filePath).toString())
} else {
const filePath = request.url.slice('local://'.length)
return net.fetch(url.pathToFileURL(filePath).toString())
}
})
// Set app user model id for windows // Set app user model id for windows
electronApp.setAppUserModelId('top.fatweb') electronApp.setAppUserModelId('top.fatweb')
@@ -73,3 +146,4 @@ app.on('window-all-closed', () => {
// In this file you can include the rest of your app's specific main process // In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here. // code. You can also put them in separate files and require them here.
import './dataProcess'

View File

@@ -1,23 +1,27 @@
import { contextBridge, Notification } from 'electron' import { contextBridge, Notification, ipcRenderer } from 'electron'
import { electronAPI } from '@electron-toolkit/preload' import { electronAPI } from '@electron-toolkit/preload'
// Custom APIs for renderer // Custom APIs for renderer
const api = {} const api = {
installTool: (newTools: Record<string, Record<Platform, ToolVo>>) =>
ipcRenderer.invoke('store:installTool', newTools),
getInstalledTool: () => ipcRenderer.invoke('store:getInstalledTool')
}
// Use `contextBridge` APIs to expose Electron APIs to // Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise // renderer only if context isolation is enabled, otherwise
// just add to the DOM global. // just add to the DOM global.
if (process.contextIsolated) { if (process.contextIsolated) {
try { try {
contextBridge.exposeInMainWorld('electron', electronAPI) contextBridge.exposeInMainWorld('electronAPI', electronAPI)
contextBridge.exposeInMainWorld('api', api) contextBridge.exposeInMainWorld('api', api)
contextBridge.exposeInMainWorld('notification', Notification) contextBridge.exposeInMainWorld('Notification', Notification)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }
} else { } else {
// @ts-expect-error (define in dts) // @ts-expect-error (define in dts)
window.electron = electronAPI window.electronAPI = electronAPI
// @ts-expect-error (define in dts) // @ts-expect-error (define in dts)
window.api = api window.api = api
// @ts-expect-error (define in dts) // @ts-expect-error (define in dts)

View File

@@ -3,6 +3,7 @@ import { getRedirectUrl } from '@/util/route'
import { getLoginStatus, getVerifyStatus_async } from '@/util/auth' import { getLoginStatus, getVerifyStatus_async } from '@/util/auth'
const AuthRoute = () => { const AuthRoute = () => {
const navigate = useNavigate()
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()
const matches = useMatches() const matches = useMatches()
const lastMatch = matches.reduce((_, second) => second) const lastMatch = matches.reduce((_, second) => second)
@@ -12,6 +13,12 @@ const AuthRoute = () => {
const isLogin = getLoginStatus() const isLogin = getLoginStatus()
const isVerify = getVerifyStatus_async() const isVerify = getVerifyStatus_async()
useEffect(() => {
window.electronAPI.ipcRenderer.on('open-url', (_, url: string) => {
navigate(url)
})
}, [])
return useMemo(() => { return useMemo(() => {
document.title = `${handle?.titlePrefix ?? ''}${ document.title = `${handle?.titlePrefix ?? ''}${
handle?.title ? handle?.title : PRODUCTION_NAME handle?.title ? handle?.title : PRODUCTION_NAME

View File

@@ -14,8 +14,7 @@
-ms-overflow-style: none; -ms-overflow-style: none;
.hide-scrollbar-content { .hide-scrollbar-content {
display: inline-block; min-width: 100%;
width: 100%;
} }
} }
@@ -68,7 +67,6 @@
} }
.vertical-scrollbar { .vertical-scrollbar {
padding: 12px 2px;
height: 100%; height: 100%;
left: 100%; left: 100%;
top: 0; top: 0;
@@ -80,7 +78,6 @@
} }
.horizontal-scrollbar { .horizontal-scrollbar {
padding: 4px 12px;
width: 100%; width: 100%;
left: 0; left: 0;
top: 100%; top: 100%;

View File

@@ -27,6 +27,7 @@
font-size: constants.$SIZE_ICON_SM; font-size: constants.$SIZE_ICON_SM;
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
span { span {
transform: rotateZ(180deg); transform: rotateZ(180deg);
transition: all .3s; transition: all .3s;
@@ -55,14 +56,16 @@
.scroll { .scroll {
min-height: 0; min-height: 0;
flex: 1; flex: 1;
width: 100%;
} }
ul { ul {
> li { > li, > div > li {
padding: 2px 14px;
&.item { &.item {
position: relative; position: relative;
margin: 4px 14px; font-size: 1rem;
font-size: 1.4em;
> .menu-bt { > .menu-bt {
border-radius: 8px; border-radius: 8px;
@@ -78,6 +81,10 @@
height: 40px; height: 40px;
font-size: constants.$SIZE_ICON_SM; font-size: constants.$SIZE_ICON_SM;
cursor: pointer; cursor: pointer;
img {
width: 100%;
}
} }
a { a {
@@ -86,15 +93,24 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
transition: all 0.2s; transition: all 0.2s;
background-color: constants.$origin-color;
.text { .text {
flex: 1; flex: 1;
padding-left: 8px; padding-left: 8px;
width: 0;
overflow: hidden;
text-overflow: ellipsis;
} }
&.active { &.active {
color: constants.$origin-color; 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 { .submenu {
visibility: hidden; visibility: hidden;
position: fixed; position: fixed;
padding-left: 20px; padding-left: 10px;
z-index: 10000; z-index: 10000;
animation: 0.1s ease forwards; animation: 0.1s ease forwards;
@include mixins.unique-keyframes { @include mixins.unique-keyframes {
@@ -123,24 +139,30 @@
padding: 10px 10px; padding: 10px 10px;
background-color: constants.$origin-color; background-color: constants.$origin-color;
border-radius: 8px; border-radius: 8px;
box-shadow: 2px 2px 10px 0 rgba(0,0,0,0.1);
.item { .item {
border-radius: 8px; border-radius: 8px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
padding: 0;
a { a {
display: block; display: block;
padding: 8px 16px; padding: 8px 16px;
transition: all 0.2s; transition: all 0.2s;
.text {
width: unset;
}
&.active { &.active {
color: constants.$origin-color; 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; background-color: constants.$background-color;
} }
} }
@@ -149,7 +171,7 @@
&:hover { &:hover {
> .menu-bt { > .menu-bt {
a { a:not(.active) {
background-color: constants.$background-color; background-color: constants.$background-color;
} }
} }
@@ -171,6 +193,22 @@
} }
} }
} }
.delete {
.menu-bt {
border: {
width: 1px;
color: constants.$error-secondary-color;
style: dashed;
};
filter: drop-shadow(1000px 0 0 constants.$error-secondary-color);
transform: translate(-1000px);
> a {
background-color: transparent !important;
}
}
}
} }
} }
@@ -251,13 +289,14 @@
transition: all .3s; transition: all .3s;
} }
} }
.text { .text {
display: none; display: none;
} }
} }
.menu-bt { .menu-bt {
.text { .text, .extend {
display: none; display: none;
} }
} }
@@ -272,6 +311,7 @@
.footer { .footer {
position: relative; position: relative;
.text { .text {
display: none; display: none;
} }
@@ -291,6 +331,7 @@
.icon-exit { .icon-exit {
padding: 4px 8px; padding: 4px 8px;
&:hover { &:hover {
border-radius: 8px; border-radius: 8px;
background-color: constants.$background-color; background-color: constants.$background-color;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
@use '@/assets/css/constants' as constants;
[data-component=component-load-more-card] {
cursor: pointer;
.load-more-card {
width: 100%;
height: 100%;
text-align: center;
align-items: center;
.icon {
display: flex;
font-size: constants.$SIZE_ICON_XXL;
color: constants.$production-color;
align-items: center;
transform: translateY(-20px);
}
.text {
position: absolute;
top: 60%;
font-size: 1.2em;
font-weight: bolder;
}
}
}

View File

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

View File

@@ -0,0 +1,51 @@
@use '@/assets/css/constants' as constants;
[data-component=component-repository-card] {
height: 100%;
.repository-card {
width: 100%;
height: 100%;
text-align: center;
align-items: center;
> * {
display: block;
flex: 0 0 auto;
}
.header {
display: flex;
width: 100%;
align-items: center;
padding: 10px;
.version-select {
width: 9em;
margin-right: auto;
}
>:not(.version-select) {
font-size: 1.6em;
}
}
.icon {
display: flex;
padding-top: 10px;
padding-bottom: 20px;
color: constants.$production-color;
font-size: constants.$SIZE_ICON_XL;
justify-content: center;
img {
width: constants.$SIZE_ICON_XL;
}
}
.tool-name {
font-weight: bolder;
font-size: 1.6em;
}
}
}

View File

@@ -0,0 +1,111 @@
@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;
z-index: 100;
> *:hover {
color: constants.$font-secondary-color;
}
}
}
.icon {
display: flex;
padding-top: 10px;
padding-bottom: 20px;
color: constants.$production-color;
font-size: constants.$SIZE_ICON_XL;
justify-content: center;
img {
width: constants.$SIZE_ICON_XL;
}
}
.info {
padding-top: 20px;
.tool-name {
font-weight: bolder;
font-size: 1.6em;
}
.tool-desc {
margin: {
top: 10px;
left: auto;
right: auto;
};
color: constants.$font-secondary-color;
overflow: hidden;
text-overflow: ellipsis;
max-height: 40px;
width: 80%;
}
}
.author {
display: flex;
margin-top: auto;
flex-direction: row;
justify-content: end;
padding-bottom: 10px;
gap: 10px;
.avatar {
> * {
width: 24px;
height: 24px;
}
}
.author-name {
display: flex;
align-items: center;
}
}
}
:hover {
.header {
.version {
opacity: 0;
}
.operation {
opacity: 1;
}
}
}
}

View File

@@ -2,40 +2,16 @@
[data-component=system] { [data-component=system] {
.root-content { .root-content {
padding: 30px; padding: 20px;
gap: 20px; gap: 20px;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-start; justify-content: flex-start;
> .card-box { > .card-box {
width: 200px; width: 200px;
height: 360px; height: 320px;
flex: 0 0 auto; flex: 0 0 auto;
overflow: hidden !important; overflow: hidden !important;
cursor: pointer;
.common-card {
width: 100%;
height: 100%;
margin-top: 100px;
text-align: center;
gap: 42px;
> * {
flex: 0 0 auto;
display: block;
}
.icon {
color: constants.$production-color;
font-size: constants.$SIZE_ICON_XL;
}
.text {
font-weight: bolder;
font-size: 2em;
}
}
} }
} }
} }

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
[data-component=system-tools-base] { [data-component=system-tools-base] {
.root-content { .root-content {
padding: 30px; padding: 20px;
gap: 10px; gap: 10px;
height: 100%; height: 100%;
width: 100%; width: 100%;
@@ -13,13 +13,14 @@
} }
>*:first-child { >*:first-child {
width: 0;
height: fit-content; height: fit-content;
} }
> *:nth-child(2) { > *:nth-child(2) {
position: sticky; position: sticky;
top: 20px; top: 20px;
height: calc(100vh - 60px); height: calc(100vh - 40px);
} }
.close-editor-btn { .close-editor-btn {

View File

@@ -1,5 +1,5 @@
[data-component=system-tools-code] { [data-component=system-tools-code] {
padding: 30px; padding: 20px;
.card-box { .card-box {
width: 100%; width: 100%;

View File

@@ -1,5 +1,5 @@
[data-component=system-tools-execute] { [data-component=system-tools-execute] {
padding: 30px; padding: 20px;
.card-box { .card-box {
width: 100%; width: 100%;

View File

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

View File

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

View File

@@ -43,6 +43,19 @@
} }
} }
} }
.preview {
display: flex;
position: relative;
justify-content: center;
align-items: center;
.no-preview {
font-weight: bolder;
color: constants.$font-secondary-color;
font-size: 1.4em;
}
}
} }
} }
} }

View File

@@ -1,4 +1,10 @@
[data-component=tools-edit] { [data-component=tools-edit] {
padding: 20px;
.card-box {
height: 100%;
width: 100%;
.root-content { .root-content {
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -13,10 +19,11 @@
height: 100%; height: 100%;
} }
} }
}
.draggable-content { .draggable-content {
position: fixed; position: fixed;
inset-inline-end: 32px; inset-inline-end: 48px;
inset-block-end: 48px; inset-block-end: 48px;
> * { > * {

View File

@@ -3,59 +3,18 @@
[data-component=tools] { [data-component=tools] {
.root-content { .root-content {
padding: 30px; padding: 20px;
gap: 20px;
.own-content {
gap: 20px; gap: 20px;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-start; justify-content: flex-start;
> .card-box { > .card-box, > div {
width: 180px; width: 180px;
height: 290px; height: 290px;
flex: 0 0 auto; 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: 8em;
}
.upgrade-bt {
position: absolute;
top: 10px;
right: 10px;
font-size: 1.8em;
}
.icon {
display: flex;
padding-top: 50px;
padding-bottom: 20px;
color: constants.$production-color;
font-size: constants.$SIZE_ICON_XL;
justify-content: center;
img {
width: constants.$SIZE_ICON_XL;
}
}
.tool-name {
font-weight: bolder;
font-size: 1.6em;
}
}
} }
@@ -112,4 +71,40 @@
} }
} }
} }
.favorite-divider {
display: flex;
flex-direction: row;
align-items: center;
gap: 20px;
margin-top: 20px;
:first-child, :last-child {
height: 0;
border: {
width: 1px;
color: constants.$divide-color;
style: dashed;
};
}
.divider-text {
flex: 0 0 auto;
font-size: 1.2em;
color: constants.$font-secondary-color;
}
}
.star-content {
gap: 20px;
flex-wrap: wrap;
justify-content: flex-start;
> .card-box, > div {
width: 180px;
height: 290px;
flex: 0 0 auto;
}
}
}
} }

View File

@@ -0,0 +1,49 @@
@use '@/assets/css/constants' as constants;
[data-component=tools-local] {
.search {
display: flex;
position: sticky;
width: 100%;
margin-top: 20px;
top: 20px;
z-index: 10;
justify-content: center;
transition: all 0.3s ease;
> * {
width: 80%;
}
&.hide {
transform: translateY(-60px);
}
}
.root-content {
padding: 20px;
gap: 20px;
flex-wrap: wrap;
justify-content: center;
> div {
width: 180px;
height: 290px;
flex: 0 0 auto;
}
.no-tool {
display: flex;
justify-content: center;
font-size: 1.4em;
font-weight: bolder;
color: constants.$font-secondary-color;
}
}
.android-qrcode {
align-items: center;
transform: translateX(-16px);
gap: 20px;
}
}

View File

@@ -1,5 +1,5 @@
[data-component=tools-source] { [data-component=tools-source] {
padding: 30px; padding: 20px;
.card-box { .card-box {
width: 100%; width: 100%;

View File

@@ -21,116 +21,15 @@
} }
.root-content { .root-content {
padding: 30px; padding: 20px;
gap: 20px; gap: 20px;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
> .card-box { > div {
width: 180px; width: 180px;
height: 290px; height: 290px;
flex: 0 0 auto; flex: 0 0 auto;
cursor: pointer;
.common-card {
width: 100%;
height: 100%;
text-align: center;
align-items: center;
> * {
display: block;
flex: 0 0 auto;
}
.icon {
display: flex;
padding-top: 40px;
padding-bottom: 20px;
color: constants.$production-color;
font-size: constants.$SIZE_ICON_XL;
justify-content: center;
img {
width: constants.$SIZE_ICON_XL;
}
}
.version {
position: absolute;
left: 10px;
top: 10px;
}
.info {
padding-top: 20px;
.tool-name {
font-weight: bolder;
font-size: 1.6em;
}
.tool-desc {
margin-top: 10px;
color: constants.$font-secondary-color;
}
}
.author {
display: flex;
margin-top: auto;
flex-direction: row;
justify-content: end;
padding-bottom: 10px;
gap: 10px;
.avatar {
> * {
width: 24px;
height: 24px;
}
}
.author-name {
display: flex;
align-items: center;
}
}
.operation {
position: absolute;
top: 6px;
right: 12px;
font-size: 1.6em;
> *:hover {
color: constants.$font-secondary-color;
}
}
}
.load-more-card {
width: 100%;
height: 100%;
text-align: center;
align-items: center;
.icon {
display: flex;
font-size: constants.$SIZE_ICON_XXL;
color: constants.$production-color;
align-items: center;
transform: translateY(-20px);
}
.text {
position: absolute;
top: 60%;
font-size: 1.2em;
font-weight: bolder;
}
}
} }
.no-tool { .no-tool {
@@ -141,4 +40,10 @@
color: constants.$font-secondary-color; color: constants.$font-secondary-color;
} }
} }
.android-qrcode {
align-items: center;
transform: translateX(-16px);
gap: 20px;
}
} }

View File

@@ -3,9 +3,9 @@
[data-component=tools-store-user] .root-content { [data-component=tools-store-user] .root-content {
padding: { padding: {
top: 80px; top: 80px;
left: 30px; left: 20px;
right: 30px; right: 20px;
bottom: 30px; bottom: 20px;
}; };
.root-box { .root-box {
@@ -68,111 +68,10 @@
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
> .card-box { > div {
width: 180px; width: 180px;
height: 290px; height: 290px;
flex: 0 0 auto; flex: 0 0 auto;
cursor: pointer;
.common-card {
width: 100%;
height: 100%;
text-align: center;
align-items: center;
> * {
display: block;
flex: 0 0 auto;
}
.icon {
display: flex;
padding-top: 40px;
padding-bottom: 20px;
color: constants.$production-color;
font-size: constants.$SIZE_ICON_XL;
justify-content: center;
img {
width: constants.$SIZE_ICON_XL;
}
}
.version {
position: absolute;
left: 10px;
top: 10px;
}
.info {
padding-top: 20px;
.tool-name {
font-weight: bolder;
font-size: 1.6em;
}
.tool-desc {
margin-top: 10px;
color: constants.$font-secondary-color;
}
}
.author {
display: flex;
margin-top: auto;
flex-direction: row;
justify-content: end;
padding-bottom: 10px;
gap: 10px;
.avatar {
> * {
width: 24px;
height: 24px;
}
}
.author-name {
display: flex;
align-items: center;
}
}
.operation {
position: absolute;
top: 6px;
right: 12px;
font-size: 1.6em;
> *:hover {
color: constants.$font-secondary-color;
}
}
}
.load-more-card {
width: 100%;
height: 100%;
text-align: center;
align-items: center;
.icon {
display: flex;
font-size: constants.$SIZE_ICON_XXL;
color: constants.$production-color;
align-items: center;
transform: translateY(-20px);
}
.text {
position: absolute;
top: 60%;
font-size: 1.2em;
font-weight: bolder;
}
}
} }
.no-tool { .no-tool {
@@ -185,4 +84,10 @@
} }
} }
} }
.android-qrcode {
align-items: center;
transform: translateX(-16px);
gap: 20px;
}
} }

View File

@@ -1,7 +1,8 @@
[data-component=tools-view] { [data-component=tools-view] {
padding: 30px; padding: 20px;
.card-box { .card-box {
position: relative;
height: 100%; height: 100%;
width: 100%; width: 100%;
} }

View File

@@ -3,9 +3,9 @@
[data-component=user] .root-content { [data-component=user] .root-content {
padding: { padding: {
top: 80px; top: 80px;
left: 30px; left: 20px;
right: 30px; right: 20px;
bottom: 30px; bottom: 20px;
}; };
.card-box { .card-box {
@@ -70,6 +70,8 @@
} }
.url { .url {
cursor: pointer;
> span { > span {
margin-left: 8px; margin-left: 8px;
} }

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M482.133333 738.133333L136.533333 392.533333c-17.066667-17.066667-17.066667-42.666667 0-59.733333 8.533333-8.533333 19.2-12.8 29.866667-12.8h689.066667c23.466667 0 42.666667 19.2 42.666666 42.666667 0 10.666667-4.266667 21.333333-12.8 29.866666L541.866667 738.133333c-17.066667 17.066667-42.666667 17.066667-59.733334 0z" /></svg>

After

Width:  |  Height:  |  Size: 404 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M512 885.76a392.2944 392.2944 0 1 1 392.192-392.192A392.6016 392.6016 0 0 1 512 885.76z m0-702.5664A310.3744 310.3744 0 1 0 822.6816 493.568 310.6816 310.6816 0 0 0 512 183.1936z" /><path d="M978.432 263.8848c-22.9376-46.6944-127.2832-36.5568-193.7408-11.3664l29.0816 76.5952c4.3008-1.6384 8.6016-3.1744 12.8-4.5056l-8.704 5.4272c-76.0832 47.0016-187.0848 107.1104-304.64 165.0688S280.2688 604.16 196.7104 636.1088l-2.1504 0.8192-30.72-49.5616C91.136 632.2176 22.4256 674.7136 46.3872 723.1488c9.216 18.8416 26.3168 26.112 48.3328 26.112 34.816 0 81.92-18.0224 131.072-36.5568 87.1424-33.0752 202.0352-84.1728 323.584-144.0768S781.6192 448.7168 860.8768 399.36c72.8064-44.544 141.5168-87.04 117.5552-135.4752zM125.7472 675.0208l24.064-26.624 11.3664 26.624z m739.7376-350.5152a98.304 98.304 0 0 0-18.2272-32.4608 152.1664 152.1664 0 0 1 57.0368-10.24 115.5072 115.5072 0 0 1-38.8096 42.7008z" /></svg>

After

Width:  |  Height:  |  Size: 975 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M810.666667 128H213.333333a128 128 0 0 0-128 128v341.333333a128 128 0 0 0 128 128h256v85.333334H298.666667a42.666667 42.666667 0 0 0 0 85.333333h426.666666a42.666667 42.666667 0 0 0 0-85.333333h-170.666666v-85.333334h256a128 128 0 0 0 128-128V256a128 128 0 0 0-128-128z m42.666666 469.333333a42.666667 42.666667 0 0 1-42.666666 42.666667H213.333333a42.666667 42.666667 0 0 1-42.666666-42.666667V256a42.666667 42.666667 0 0 1 42.666666-42.666667h597.333334a42.666667 42.666667 0 0 1 42.666666 42.666667z" /></svg>

After

Width:  |  Height:  |  Size: 586 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M554.667 554.667V792.96l77.994-77.995 60.374 60.374L512 956.33 330.965 775.339l60.374-60.374 77.994 77.995V554.667h85.334zM512 85.333a298.71 298.71 0 0 1 296.704 264.278 234.667 234.667 0 0 1-40.661 460.117v-85.93a149.333 149.333 0 1 0-47.446-294.827 213.333 213.333 0 1 0-417.152 0 149.333 149.333 0 0 0-55.125 293.546l7.68 1.28v85.931a234.667 234.667 0 0 1-40.704-460.117A298.667 298.667 0 0 1 512 85.333z" /></svg>

After

Width:  |  Height:  |  Size: 491 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M7 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 2zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 14zm6-8a2 2 0 1 0-.001-4.001A2 2 0 0 0 13 6zm0 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 14z" /></svg>

After

Width:  |  Height:  |  Size: 325 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M512 938.715429A426.642286 426.642286 0 1 1 512 85.357714a426.642286 426.642286 0 0 1 0 853.357715z m-42.642286-469.357715v256h85.284572v-256H469.284571z m0-170.642285V384h85.284572V298.715429H469.284571z" /></svg>

After

Width:  |  Height:  |  Size: 288 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M544 531.2V704c0 19.2-12.8 32-32 32s-32-12.8-32-32V531.2L332.8 441.6c-12.8-6.4-19.2-25.6-6.4-44.8 6.4-12.8 25.6-19.2 44.8-6.4L512 473.6l140.8-83.2c12.8-6.4 32-6.4 44.8 12.8 6.4 12.8 6.4 32-12.8 44.8L544 531.2z m-12.8-428.8c-12.8-6.4-25.6-6.4-32 0L147.2 307.2c-12.8 6.4-19.2 19.2-19.2 25.6v352c0 12.8 6.4 19.2 19.2 25.6l352 211.2c12.8 6.4 25.6 6.4 32 0l352-211.2c12.8-6.4 19.2-12.8 19.2-25.6V332.8c0-12.8-6.4-19.2-19.2-25.6L531.2 102.4z m32-57.6L908.8 256c32 12.8 51.2 44.8 51.2 76.8v358.4c0 32-19.2 64-51.2 76.8l-345.6 211.2c-32 19.2-70.4 19.2-102.4 0L115.2 768c-32-12.8-51.2-44.8-51.2-76.8V332.8c0-32 19.2-64 51.2-76.8L460.8 44.8C492.8 25.6 531.2 25.6 563.2 44.8z" /></svg>

After

Width:  |  Height:  |  Size: 748 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M768 64a64 64 0 0 1 64 64v768a64 64 0 0 1-64 64H256a64 64 0 0 1-64-64V128a64 64 0 0 1 64-64h512z m-32 64H288a32 32 0 0 0-32 32v704a32 32 0 0 0 32 32h448a32 32 0 0 0 32-32V160a32 32 0 0 0-32-32z m-223.488 608a48 48 0 1 1 0 96 48 48 0 0 1 0-96z" /></svg>

After

Width:  |  Height:  |  Size: 326 B

View File

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

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M672 418H144c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32z m-44 402H188V494h440v326z" /><path d="M819.3 328.5c-78.8-100.7-196-153.6-314.6-154.2l-0.2-64c0-6.5-7.6-10.1-12.6-6.1l-128 101c-4 3.1-3.9 9.1 0 12.3L492 318.6c5.1 4 12.7 0.4 12.6-6.1v-63.9c12.9 0.1 25.9 0.9 38.8 2.5 42.1 5.2 82.1 18.2 119 38.7 38.1 21.2 71.2 49.7 98.4 84.3 27.1 34.7 46.7 73.7 58.1 115.8 11 40.7 14 82.7 8.9 124.8-0.7 5.4-1.4 10.8-2.4 16.1h74.9c14.8-103.6-11.3-213-81-302.3z" /></svg>

After

Width:  |  Height:  |  Size: 585 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M480.5 251.2c13-1.6 25.9-2.4 38.8-2.5v63.9c0 6.5 7.5 10.1 12.6 6.1L660 217.6c4-3.2 4-9.2 0-12.3l-128-101c-5.1-4-12.6-0.4-12.6 6.1l-0.2 64c-118.6 0.5-235.8 53.4-314.6 154.2-69.6 89.2-95.7 198.6-81.1 302.4h74.9c-0.9-5.3-1.7-10.7-2.4-16.1-5.1-42.1-2.1-84.1 8.9-124.8 11.4-42.2 31-81.1 58.1-115.8 27.2-34.7 60.3-63.2 98.4-84.3 37-20.6 76.9-33.6 119.1-38.8z" /><path d="M880 418H352c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32z m-44 402H396V494h440v326z" /></svg>

After

Width:  |  Height:  |  Size: 585 B

View File

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

After

Width:  |  Height:  |  Size: 856 B

View File

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

After

Width:  |  Height:  |  Size: 615 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M720 464H560V304a48 48 0 1 0-96 0v160H304a48 48 0 1 0 0 96h160v160a48 48 0 1 0 96 0V560h160a48 48 0 1 0 0-96zM512 0C229.232 0 0 229.232 0 512c0 282.768 229.232 512 512 512h464a48 48 0 0 0 48-48V512.032 512C1024 229.232 794.768 0 512 0z m416 512.016C928 741.744 741.776 927.968 512.064 928H512C282.256 928 96 741.76 96 512 96 282.256 282.256 96 512 96s416 186.256 416 416.016z" /></svg>

After

Width:  |  Height:  |  Size: 459 B

View File

@@ -76,11 +76,11 @@ export const useEditor = () => {
export const useTypesProgress = () => { export const useTypesProgress = () => {
const [progress, setProgress] = useState(0) const [progress, setProgress] = useState(0)
const [total, setTotal] = useState(0) const [total, setTotal] = useState(0)
const [finished, setFinished] = useState(false) const [isFinished, setIsFinished] = useState(false)
const onWatch = (typeHelper: TypeHelper) => { const onWatch = (typeHelper: TypeHelper) => {
const handleStarted = () => { const handleStarted = () => {
setFinished(false) setIsFinished(false)
} }
typeHelper.addListener('started', handleStarted) typeHelper.addListener('started', handleStarted)
@@ -91,7 +91,7 @@ export const useTypesProgress = () => {
typeHelper.addListener('progress', handleProgress) typeHelper.addListener('progress', handleProgress)
const handleFinished = () => { const handleFinished = () => {
setFinished(true) setIsFinished(true)
} }
typeHelper.addListener('progress', handleFinished) typeHelper.addListener('progress', handleFinished)
@@ -105,7 +105,7 @@ export const useTypesProgress = () => {
return { return {
progress, progress,
total, total,
finished, finished: isFinished,
onWatch onWatch
} }
} }

View File

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

View File

@@ -31,7 +31,7 @@ const Item = ({
}: ItemProps) => { }: ItemProps) => {
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
const [fileName, setFileName] = useState(value) const [fileName, setFileName] = useState(value)
const [creating, setCreating] = useState(prop.creating) const [isCreating, setIsCreating] = useState(prop.creating)
const handleOnClick = () => { const handleOnClick = () => {
if (hasEditing) { if (hasEditing) {
@@ -52,35 +52,35 @@ const Item = ({
} }
const finishNameFile = () => { const finishNameFile = () => {
if (!creating || onValidate ? !onValidate?.(fileName, value) : false) { if (!isCreating || onValidate ? !onValidate?.(fileName, value) : false) {
inputRef.current?.focus() inputRef.current?.focus()
return return
} }
if (fileName === value && active) { if (fileName === value && active) {
setCreating(false) setIsCreating(false)
setHasEditing?.(false) setHasEditing?.(false)
return return
} }
onOk?.(fileName) onOk?.(fileName)
setCreating(false) setIsCreating(false)
setHasEditing?.(false) setHasEditing?.(false)
} }
const cancelNameFile = () => { const cancelNameFile = () => {
setFileName(value) setFileName(value)
setCreating(false) setIsCreating(false)
setHasEditing?.(false) setHasEditing?.(false)
onCancel?.() onCancel?.()
} }
const handleOnDoubleClick = () => { const handleOnDoubleClick = () => {
if (readonly || creating || hasEditing) { if (readonly || isCreating || hasEditing) {
return return
} }
setCreating(true) setIsCreating(true)
setHasEditing?.(true) setHasEditing?.(true)
setFileName(value) setFileName(value)
setTimeout(() => { setTimeout(() => {
@@ -112,7 +112,7 @@ const Item = ({
className={`tab-item${active ? ' active' : ''}${className ? ` ${className}` : ''}`} className={`tab-item${active ? ' active' : ''}${className ? ` ${className}` : ''}`}
onClick={handleOnClick} onClick={handleOnClick}
> >
{creating ? ( {isCreating ? (
<div className={'tab-item-input'}> <div className={'tab-item-input'}>
<input <input
ref={inputRef} ref={inputRef}

View File

@@ -34,7 +34,7 @@ const FileSelector = ({
}: FileSelectorProps) => { }: FileSelectorProps) => {
const hideScrollbarRef = useRef<HideScrollbarElement>(null) const hideScrollbarRef = useRef<HideScrollbarElement>(null)
const [tabs, setTabs] = useState<string[]>([]) const [tabs, setTabs] = useState<string[]>([])
const [creating, setCreating] = useState(false) const [isCreating, setIsCreating] = useState(false)
const [hasEditing, setHasEditing] = useState(false) const [hasEditing, setHasEditing] = useState(false)
const getMaxSequenceTabName = (filesName: string[]) => { const getMaxSequenceTabName = (filesName: string[]) => {
@@ -56,7 +56,7 @@ const FileSelector = ({
} }
setTabs([...tabs, getMaxSequenceTabName(tabs)]) setTabs([...tabs, getMaxSequenceTabName(tabs)])
setCreating(true) setIsCreating(true)
setTimeout(() => { setTimeout(() => {
hideScrollbarRef.current?.scrollRight(1000) hideScrollbarRef.current?.scrollRight(1000)
}) })
@@ -64,16 +64,16 @@ const FileSelector = ({
const handleOnCancel = () => { const handleOnCancel = () => {
onError?.('') onError?.('')
if (!creating) { if (!isCreating) {
return return
} }
tabs.pop() tabs.pop()
setTabs([...tabs]) setTabs([...tabs])
setCreating(false) setIsCreating(false)
} }
const handleOnClickTab = (fileName: string) => { const handleOnClickTab = (fileName: string) => {
if (creating) { if (isCreating) {
return return
} }
@@ -97,9 +97,9 @@ const FileSelector = ({
} }
const handleOnSaveTab = (value: string, item: string) => { const handleOnSaveTab = (value: string, item: string) => {
if (creating) { if (isCreating) {
onAddFile?.(value) onAddFile?.(value)
setCreating(false) setIsCreating(false)
} else { } else {
onUpdateFileName?.(value, item) onUpdateFileName?.(value, item)
} }
@@ -166,14 +166,20 @@ const FileSelector = ({
<> <>
<div data-component={'playground-file-selector'} className={'tab'}> <div data-component={'playground-file-selector'} className={'tab'}>
<div className={'multiple'}> <div className={'multiple'}>
<HideScrollbar ref={hideScrollbarRef}> <HideScrollbar
ref={hideScrollbarRef}
autoHideWaitingTime={800}
scrollbarWidth={1}
scrollbarAsidePadding={0}
scrollbarEdgePadding={0}
>
<FlexBox direction={'horizontal'} className={'tab-content'}> <FlexBox direction={'horizontal'} className={'tab-content'}>
{tabs.map((item, index) => ( {tabs.map((item, index) => (
<Item <Item
key={index + item} key={index + item}
value={item} value={item}
active={selectedFileName === item} active={selectedFileName === item}
creating={creating} creating={isCreating}
readonly={readonly || notRemovableFiles.includes(item)} readonly={readonly || notRemovableFiles.includes(item)}
hasEditing={hasEditing} hasEditing={hasEditing}
setHasEditing={setHasEditing} setHasEditing={setHasEditing}

View File

@@ -1,19 +1,30 @@
import { ChangeEvent } from 'react'
import '@/components/Playground/Output/Preview/render.scss' import '@/components/Playground/Output/Preview/render.scss'
import { COLOR_FONT_MAIN } from '@/constants/common.constants'
import iframeRaw from '@/components/Playground/Output/Preview/iframe.html?raw' import iframeRaw from '@/components/Playground/Output/Preview/iframe.html?raw'
import HideScrollbar from '@/components/common/HideScrollbar'
interface RenderProps { interface RenderProps {
iframeKey: string iframeKey: string
compiledCode: string compiledCode: string
mobileMode?: boolean
} }
interface IMessage { interface IMessage {
type: 'LOADED' | 'ERROR' | 'UPDATE' | 'DONE' type: 'LOADED' | 'ERROR' | 'UPDATE' | 'DONE' | 'SCALE'
msg: string msg: string
data: { data: {
compiledCode: string compiledCode?: string
zoom?: number
} }
} }
interface IDevice {
name: string
width: number
height: number
}
const getIframeUrl = (iframeRaw: string) => { const getIframeUrl = (iframeRaw: string) => {
const shimsUrl = '//unpkg.com/es-module-shims/dist/es-module-shims.js' const shimsUrl = '//unpkg.com/es-module-shims/dist/es-module-shims.js'
// 判断浏览器是否支持esm 不支持esm就引入es-module-shims // 判断浏览器是否支持esm 不支持esm就引入es-module-shims
@@ -29,12 +40,115 @@ const getIframeUrl = (iframeRaw: string) => {
const iframeUrl = getIframeUrl(iframeRaw) const iframeUrl = getIframeUrl(iframeRaw)
const Render = ({ iframeKey, compiledCode }: RenderProps) => { const Render = ({ iframeKey, compiledCode, mobileMode = false }: RenderProps) => {
const iframeRef = useRef<HTMLIFrameElement>(null) const iframeRef = useRef<HTMLIFrameElement>(null)
const [loaded, setLoaded] = useState(false) const [isLoaded, setIsLoaded] = useState(false)
const [selectedDevice, setSelectedDevice] = useState('Pixel 7')
const [zoom, setZoom] = useState(1)
const [isRotate, setIsRotate] = useState(false)
const devices: IDevice[] = [
{
name: 'iPhone SE',
width: 375,
height: 667
},
{
name: 'iPhone XR',
width: 414,
height: 896
},
{
name: 'iPhone 12 Pro',
width: 390,
height: 844
},
{
name: 'iPhone 14 Pro Max',
width: 430,
height: 932
},
{
name: 'Pixel 7',
width: 412,
height: 915
},
{
name: 'Samsung Galaxy S8+',
width: 360,
height: 740
},
{
name: 'Samsung Galaxy S20 Ultra',
width: 412,
height: 915
},
{
name: 'iPad Mini',
width: 768,
height: 1024
},
{
name: 'iPad Air',
width: 820,
height: 1180
},
{
name: 'iPad Pro',
width: 1024,
height: 1366
},
{
name: 'Surface Pro 7',
width: 912,
height: 1368
},
{
name: 'Surface Duo',
width: 540,
height: 720
},
{
name: 'Galaxy Fold',
width: 280,
height: 653
},
{
name: 'Asus Zenbook Fold',
width: 853,
height: 1280
},
{
name: 'Samsung Galaxy A51/71',
width: 412,
height: 914
},
{
name: 'Nest Hub',
width: 1024,
height: 600
},
{
name: 'Nest Hub Max',
width: 1280,
height: 800
}
]
const handleOnChangeDevice = (e: ChangeEvent<HTMLSelectElement>) => {
setSelectedDevice(e.target.value)
}
const handleOnChangeZoom = (e: ChangeEvent<HTMLInputElement>) => {
setZoom(Number(e.target.value))
}
const handleOnRotateDevice = () => {
setIsRotate(!isRotate)
}
useEffect(() => { useEffect(() => {
if (loaded) { if (isLoaded) {
iframeRef.current?.contentWindow?.postMessage( iframeRef.current?.contentWindow?.postMessage(
{ {
type: 'UPDATE', type: 'UPDATE',
@@ -43,16 +157,97 @@ const Render = ({ iframeKey, compiledCode }: RenderProps) => {
'*' '*'
) )
} }
}, [compiledCode, loaded]) }, [isLoaded, compiledCode])
return ( useEffect(() => {
if (isLoaded) {
iframeRef.current?.contentWindow?.postMessage(
{
type: 'SCALE',
data: { zoom: zoom }
} as IMessage,
'*'
)
}
}, [isLoaded, zoom])
return mobileMode ? (
<>
<HideScrollbar
className={'mobile-mode-background'}
isShowVerticalScrollbar
isShowHorizontalScrollbar
autoHideWaitingTime={1000}
>
<div className={'mobile-mode-content'} style={{ zoom }}>
<div className={`device${isRotate ? ' rotate' : ''}`}>
<div className={`device-header${isRotate ? ' rotate' : ''}`} />
<div
className={`device-content${isRotate ? ' rotate' : ''}`}
style={{
width: isRotate
? devices.find((value) => value.name === selectedDevice)
?.height ?? 915
: devices.find((value) => value.name === selectedDevice)
?.width ?? 412,
height: isRotate
? devices.find((value) => value.name === selectedDevice)
?.width ?? 412
: devices.find((value) => value.name === selectedDevice)
?.height ?? 915
}}
>
<iframe <iframe
data-component={'playground-output-preview-render'} data-component={'playground-output-preview-render'}
key={iframeKey} key={iframeKey}
ref={iframeRef} ref={iframeRef}
src={iframeUrl} src={iframeUrl}
onLoad={() => setLoaded(true)} onLoad={() => setIsLoaded(true)}
sandbox="allow-downloads allow-forms allow-modals allow-scripts" sandbox="allow-downloads allow-forms allow-modals allow-scripts"
allow={'clipboard-read; clipboard-write'}
/>
</div>
<div className={`device-footer${isRotate ? ' rotate' : ''}`} />
</div>
</div>
</HideScrollbar>
<div className={'switch-device'}>
<IconOxygenMobile fill={COLOR_FONT_MAIN} />
<select value={selectedDevice} onChange={handleOnChangeDevice}>
{devices.map((value) => (
<option value={value.name}>{value.name}</option>
))}
</select>
<div className={'rotate-device'} title={'旋转屏幕'} onClick={handleOnRotateDevice}>
{isRotate ? (
<IconOxygenRotateRight fill={COLOR_FONT_MAIN} />
) : (
<IconOxygenRotateLeft fill={COLOR_FONT_MAIN} />
)}
</div>
</div>
<div className={'switch-zoom'}>
<IconOxygenZoom fill={COLOR_FONT_MAIN} />
<input
type={'range'}
min={0.5}
max={2}
step={0.1}
value={zoom}
onChange={handleOnChangeZoom}
/>
</div>
</>
) : (
<iframe
data-component={'playground-output-preview-render'}
key={iframeKey}
ref={iframeRef}
src={iframeUrl}
onLoad={() => setIsLoaded(true)}
sandbox="allow-downloads allow-forms allow-modals allow-scripts"
allow={'clipboard-read; clipboard-write'}
/> />
) )
} }

View File

@@ -11,7 +11,7 @@
window.addEventListener("message", ({ data }) => { window.addEventListener("message", ({ data }) => {
if (data?.type === "UPDATE") { if (data?.type === "UPDATE") {
// Record old styles that need to be removed // Record old styles that need to be removed
const appStyleElement = document.querySelectorAll("style[id^=\"style_\"]") || []; const appStyleElement = document.querySelectorAll("style:not(style[id$=\"_oxygen_base_style.css\"])") || [];
// Remove old app // Remove old app
const appSrcElement = document.querySelector("#appSrc"); const appSrcElement = document.querySelector("#appSrc");
@@ -36,6 +36,10 @@
document.body.appendChild(script); document.body.appendChild(script);
URL.revokeObjectURL(oldSrc); URL.revokeObjectURL(oldSrc);
} }
if (data?.type === "SCALE") {
document.getElementById("root").style.zoom = data.data.zoom
}
}); });
</script> </script>
<script type="module" id="appSrc"></script> <script type="module" id="appSrc"></script>

View File

@@ -10,6 +10,7 @@ interface PreviewProps {
entryPoint: string entryPoint: string
preExpansionCode?: string preExpansionCode?: string
postExpansionCode?: string postExpansionCode?: string
mobileMode?: boolean
} }
const Preview = ({ const Preview = ({
@@ -18,7 +19,8 @@ const Preview = ({
importMap, importMap,
entryPoint, entryPoint,
preExpansionCode = '', preExpansionCode = '',
postExpansionCode = '' postExpansionCode = '',
mobileMode = false
}: PreviewProps) => { }: PreviewProps) => {
const [errorMsg, setErrorMsg] = useState('') const [errorMsg, setErrorMsg] = useState('')
const [compiledCode, setCompiledCode] = useState('') const [compiledCode, setCompiledCode] = useState('')
@@ -41,7 +43,7 @@ const Preview = ({
return ( return (
<div data-component={'playground-preview'}> <div data-component={'playground-preview'}>
<Render iframeKey={iframeKey} compiledCode={compiledCode} /> <Render iframeKey={iframeKey} compiledCode={compiledCode} mobileMode={mobileMode} />
{errorMsg && <div className={'playground-error-message'}>{errorMsg}</div>} {errorMsg && <div className={'playground-error-message'}>{errorMsg}</div>}
</div> </div>
) )

View File

@@ -1,8 +1,7 @@
[data-component=playground-preview] { [data-component=playground-preview] {
display: flex; display: flex;
position: relative; position: relative;
width: 100%; height: 0;
height: 100%;
.playground-error-message { .playground-error-message {
position: absolute; position: absolute;

View File

@@ -1,6 +1,80 @@
@use '@/assets/css/constants' as constants;
[data-component=playground-output-preview-render] { [data-component=playground-output-preview-render] {
border: none; border: none;
height: 100%; height: 100%;
width: 100%; width: 100%;
flex: 1; flex: 1;
} }
.mobile-mode-background {
background-color: rgba(204, 204, 204, 0.66);
.mobile-mode-content {
padding: 80px;
}
.device {
display: flex;
flex-direction: column;
background-color: #EEEFF2;
width: fit-content;
margin: 0 auto;
border-radius: 40px;
&.rotate {
flex-direction: row;
}
.device-header {
margin: 20px auto;
width: 60px;
height: 10px;
border-radius: 5px;
background-color: #C8C9CC;
&.rotate {
margin: auto 20px;
width: 10px;
height: 60px;
}
}
.device-content {
margin: 0 10px;
background-color: white;
&.rotate {
margin: 10px 0;
}
}
.device-footer {
margin: 20px auto;
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #C8C9CC;
&.rotate {
margin: auto 20px;
}
}
}
}
.switch-device, .switch-zoom {
display: flex;
position: absolute;
top: 10px;
align-items: center;
gap: 4px;
}
.switch-device {
left: 10px;
}
.switch-zoom {
right: 10px;
}

View File

@@ -11,6 +11,7 @@ interface OutputProps {
entryPoint: string entryPoint: string
preExpansionCode?: string preExpansionCode?: string
postExpansionCode?: string postExpansionCode?: string
mobileMode?: boolean
} }
const Output = ({ const Output = ({
@@ -19,7 +20,8 @@ const Output = ({
importMap, importMap,
entryPoint, entryPoint,
preExpansionCode, preExpansionCode,
postExpansionCode postExpansionCode,
mobileMode = false
}: OutputProps) => { }: OutputProps) => {
const [selectedTab, setSelectedTab] = useState('Preview') const [selectedTab, setSelectedTab] = useState('Preview')
@@ -42,6 +44,7 @@ const Output = ({
entryPoint={entryPoint} entryPoint={entryPoint}
preExpansionCode={preExpansionCode} preExpansionCode={preExpansionCode}
postExpansionCode={postExpansionCode} postExpansionCode={postExpansionCode}
mobileMode={mobileMode}
/> />
)} )}
{selectedTab === 'Transform' && <Transform file={files[selectedFileName]} />} {selectedTab === 'Transform' && <Transform file={files[selectedFileName]} />}

View File

@@ -1,6 +1,7 @@
import esbuild, { Loader, OnLoadArgs, Plugin, PluginBuild } from 'esbuild-wasm' import esbuild, { Loader, OnLoadArgs, Plugin, PluginBuild } from 'esbuild-wasm'
import localforage from 'localforage' import localforage from 'localforage'
import axios from 'axios' import axios from 'axios'
import esbuildWasmUrl from 'esbuild-wasm/esbuild.wasm?url'
import { IFiles, IImportMap } from '@/components/Playground/shared' import { IFiles, IImportMap } from '@/components/Playground/shared'
import { cssToJs, jsonToJs, addReactImport } from '@/components/Playground/files' import { cssToJs, jsonToJs, addReactImport } from '@/components/Playground/files'
@@ -16,7 +17,7 @@ class Compiler {
void esbuild void esbuild
.initialize({ .initialize({
worker: true, worker: true,
wasmURL: 'https://esm.sh/esbuild-wasm@0.20.1/esbuild.wasm' wasmURL: esbuildWasmUrl
}) })
.finally(() => { .finally(() => {
this.init = true this.init = true

View File

@@ -21,6 +21,8 @@ interface HideScrollbarProps
minHeight?: string | number minHeight?: string | number
scrollbarWidth?: string | number scrollbarWidth?: string | number
autoHideWaitingTime?: number autoHideWaitingTime?: number
scrollbarAsidePadding?: number
scrollbarEdgePadding?: number
} }
export interface HideScrollbarElement { export interface HideScrollbarElement {
@@ -74,6 +76,8 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
children, children,
style, style,
className, className,
scrollbarAsidePadding = 12,
scrollbarEdgePadding = 4,
...props ...props
}, },
ref ref
@@ -179,16 +183,16 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
const [verticalScrollbarWidth, setVerticalScrollbarWidth] = useState(0) const [verticalScrollbarWidth, setVerticalScrollbarWidth] = useState(0)
const [verticalScrollbarLength, setVerticalScrollbarLength] = useState(100) const [verticalScrollbarLength, setVerticalScrollbarLength] = useState(100)
const [verticalScrollbarPosition, setVerticalScrollbarPosition] = useState(0) const [verticalScrollbarPosition, setVerticalScrollbarPosition] = useState(0)
const [verticalScrollbarOnClick, setVerticalScrollbarOnClick] = useState(false) const [isVerticalScrollbarOnClick, setIsVerticalScrollbarOnClick] = useState(false)
const [verticalScrollbarOnTouch, setVerticalScrollbarOnTouch] = useState(false) const [isVerticalScrollbarOnTouch, setIsVerticalScrollbarOnTouch] = useState(false)
const [verticalScrollbarAutoHide, setVerticalScrollbarAutoHide] = useState(false) const [isVerticalScrollbarAutoHide, setIsVerticalScrollbarAutoHide] = useState(false)
const [horizontalScrollbarWidth, setHorizontalScrollbarWidth] = useState(0) const [horizontalScrollbarWidth, setHorizontalScrollbarWidth] = useState(0)
const [horizontalScrollbarLength, setHorizontalScrollbarLength] = useState(100) const [horizontalScrollbarLength, setHorizontalScrollbarLength] = useState(100)
const [horizontalScrollbarPosition, setHorizontalScrollbarPosition] = useState(0) const [horizontalScrollbarPosition, setHorizontalScrollbarPosition] = useState(0)
const [horizontalScrollbarOnClick, setHorizontalScrollbarOnClick] = useState(false) const [isHorizontalScrollbarOnClick, setIsHorizontalScrollbarOnClick] = useState(false)
const [horizontalScrollbarOnTouch, setHorizontalScrollbarOnTouch] = useState(false) const [isHorizontalScrollbarOnTouch, setIsHorizontalScrollbarOnTouch] = useState(false)
const [horizontalScrollbarAutoHide, setHorizontalScrollbarAutoHide] = useState(false) const [isHorizontalScrollbarAutoHide, setIsHorizontalScrollbarAutoHide] = useState(false)
const isPreventAnyScroll = const isPreventAnyScroll =
isPreventScroll || isPreventVerticalScroll || isPreventHorizontalScroll isPreventScroll || isPreventVerticalScroll || isPreventHorizontalScroll
@@ -197,10 +201,10 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
if (autoHideWaitingTime === undefined) { if (autoHideWaitingTime === undefined) {
return return
} }
setVerticalScrollbarAutoHide(false) setIsVerticalScrollbarAutoHide(false)
if (autoHideWaitingTime > 0) { if (autoHideWaitingTime > 0) {
setTimeout(() => { setTimeout(() => {
setVerticalScrollbarAutoHide(true) setIsVerticalScrollbarAutoHide(true)
}, autoHideWaitingTime) }, autoHideWaitingTime)
} }
}, [autoHideWaitingTime, verticalScrollbarPosition]) }, [autoHideWaitingTime, verticalScrollbarPosition])
@@ -209,10 +213,10 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
if (autoHideWaitingTime === undefined) { if (autoHideWaitingTime === undefined) {
return return
} }
setHorizontalScrollbarAutoHide(false) setIsHorizontalScrollbarAutoHide(false)
if (autoHideWaitingTime > 0) { if (autoHideWaitingTime > 0) {
setTimeout(() => { setTimeout(() => {
setHorizontalScrollbarAutoHide(true) setIsHorizontalScrollbarAutoHide(true)
}, autoHideWaitingTime) }, autoHideWaitingTime)
} }
}, [autoHideWaitingTime, horizontalScrollbarPosition]) }, [autoHideWaitingTime, horizontalScrollbarPosition])
@@ -314,20 +318,20 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
} }
switch (scrollbarFlag) { switch (scrollbarFlag) {
case 'vertical': case 'vertical':
setVerticalScrollbarOnClick(true) setIsVerticalScrollbarOnClick(true)
break break
case 'horizontal': case 'horizontal':
setHorizontalScrollbarOnClick(true) setIsHorizontalScrollbarOnClick(true)
break break
} }
break break
case 'up': case 'up':
case 'leave': case 'leave':
setVerticalScrollbarOnClick(false) setIsVerticalScrollbarOnClick(false)
setHorizontalScrollbarOnClick(false) setIsHorizontalScrollbarOnClick(false)
break break
case 'move': case 'move':
if (verticalScrollbarOnClick) { if (isVerticalScrollbarOnClick) {
rootRef.current?.scrollTo({ rootRef.current?.scrollTo({
top: top:
rootRef.current?.scrollTop + rootRef.current?.scrollTop +
@@ -337,7 +341,7 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
behavior: 'instant' behavior: 'instant'
}) })
} }
if (horizontalScrollbarOnClick) { if (isHorizontalScrollbarOnClick) {
rootRef.current?.scrollTo({ rootRef.current?.scrollTo({
left: left:
rootRef.current?.scrollLeft + rootRef.current?.scrollLeft +
@@ -368,23 +372,23 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
} }
switch (scrollbarFlag) { switch (scrollbarFlag) {
case 'vertical': case 'vertical':
setVerticalScrollbarOnTouch(true) setIsVerticalScrollbarOnTouch(true)
break break
case 'horizontal': case 'horizontal':
setHorizontalScrollbarOnTouch(true) setIsHorizontalScrollbarOnTouch(true)
break break
} }
break break
case 'end': case 'end':
case 'cancel': case 'cancel':
setVerticalScrollbarOnTouch(false) setIsVerticalScrollbarOnTouch(false)
setHorizontalScrollbarOnTouch(false) setIsHorizontalScrollbarOnTouch(false)
break break
case 'move': case 'move':
if (event.touches.length !== 1) { if (event.touches.length !== 1) {
return return
} }
if (verticalScrollbarOnTouch) { if (isVerticalScrollbarOnTouch) {
rootRef.current?.scrollTo({ rootRef.current?.scrollTo({
top: top:
rootRef.current?.scrollTop + rootRef.current?.scrollTop +
@@ -395,7 +399,7 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
behavior: 'instant' behavior: 'instant'
}) })
} }
if (horizontalScrollbarOnTouch) { if (isHorizontalScrollbarOnTouch) {
rootRef.current?.scrollTo({ rootRef.current?.scrollTo({
left: left:
rootRef.current?.scrollLeft + rootRef.current?.scrollLeft +
@@ -567,7 +571,7 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
(!isHiddenVerticalScrollbarWhenFull || verticalScrollbarLength < 100) && ( (!isHiddenVerticalScrollbarWhenFull || verticalScrollbarLength < 100) && (
<div <div
className={`scrollbar vertical-scrollbar${ className={`scrollbar vertical-scrollbar${
verticalScrollbarAutoHide ? ' hide' : '' isVerticalScrollbarAutoHide ? ' hide' : ''
}`} }`}
style={{ style={{
height: maskRef.current height: maskRef.current
@@ -578,7 +582,8 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
? maskRef.current?.clientLeft + ? maskRef.current?.clientLeft +
maskRef.current?.clientWidth - maskRef.current?.clientWidth -
1 1
: undefined : undefined,
padding: `${scrollbarAsidePadding}px ${scrollbarEdgePadding}px`
}} }}
> >
<div className={'box'} style={{ width: scrollbarWidth }}> <div className={'box'} style={{ width: scrollbarWidth }}>
@@ -609,7 +614,7 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
horizontalScrollbarLength < 100) && ( horizontalScrollbarLength < 100) && (
<div <div
className={`scrollbar horizontal-scrollbar${ className={`scrollbar horizontal-scrollbar${
horizontalScrollbarAutoHide ? ' hide' : '' isHorizontalScrollbarAutoHide ? ' hide' : ''
}`} }`}
style={{ style={{
width: maskRef.current width: maskRef.current
@@ -620,7 +625,8 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
? maskRef.current?.clientTop + ? maskRef.current?.clientTop +
maskRef.current?.clientHeight - maskRef.current?.clientHeight -
1 1
: undefined : undefined,
padding: `${scrollbarAsidePadding}px ${scrollbarEdgePadding}px`
}} }}
> >
<div className={'box'} style={{ height: scrollbarWidth }}> <div className={'box'} style={{ height: scrollbarWidth }}>

View File

@@ -7,6 +7,7 @@ interface LoadingMaskProps extends PropsWithChildren {
hidden?: boolean hidden?: boolean
maskContent?: ReactNode maskContent?: ReactNode
} }
const LoadingMask = (props: LoadingMaskProps) => { const LoadingMask = (props: LoadingMaskProps) => {
const loadingIcon = ( const loadingIcon = (
<> <>

View File

@@ -2,6 +2,7 @@ import Icon from '@ant-design/icons'
import { COLOR_ERROR } from '@/constants/common.constants' import { COLOR_ERROR } from '@/constants/common.constants'
import { getRedirectUrl } from '@/util/route' import { getRedirectUrl } from '@/util/route'
import { getAvatar, getLoginStatus, getNickname, removeToken } from '@/util/auth' import { getAvatar, getLoginStatus, getNickname, removeToken } from '@/util/auth'
import { navigateToLogin, navigateToUser } from '@/util/navigation'
import { r_auth_logout } from '@/services/auth' import { r_auth_logout } from '@/services/auth'
const Footer = () => { const Footer = () => {
@@ -9,24 +10,24 @@ const Footer = () => {
const lastMatch = matches.reduce((_, second) => second) const lastMatch = matches.reduce((_, second) => second)
const location = useLocation() const location = useLocation()
const navigate = useNavigate() const navigate = useNavigate()
const [exiting, setExiting] = useState(false) const [isExiting, setIsExiting] = useState(false)
const [nickname, setNickname] = useState('') const [nickname, setNickname] = useState('')
const [avatar, setAvatar] = useState('') const [avatar, setAvatar] = useState('')
const handleClickAvatar = () => { const handleClickAvatar = () => {
if (getLoginStatus()) { if (getLoginStatus()) {
navigate('/user') navigateToUser(navigate)
} else { } else {
navigate(getRedirectUrl('/login', `${lastMatch.pathname}${location.search}`)) navigateToLogin(navigate, undefined, `${lastMatch.pathname}${location.search}`)
} }
} }
const handleLogout = () => { const handleLogout = () => {
if (exiting) { if (isExiting) {
return return
} }
setExiting(true) setIsExiting(true)
void r_auth_logout().finally(() => { void r_auth_logout().finally(() => {
removeToken() removeToken()
notification.info({ notification.info({
@@ -82,8 +83,8 @@ const Footer = () => {
<div className={'content'}> <div className={'content'}>
<span hidden={!getLoginStatus()} className={'icon-exit'} onClick={handleLogout}> <span hidden={!getLoginStatus()} className={'icon-exit'} onClick={handleLogout}>
<Icon <Icon
component={exiting ? IconOxygenLoading : IconOxygenExit} component={isExiting ? IconOxygenLoading : IconOxygenExit}
spin={exiting} spin={isExiting}
/> />
</span> </span>
</div> </div>

View File

@@ -3,16 +3,17 @@ import Icon from '@ant-design/icons'
import Submenu from '@/components/common/Sidebar/Submenu' import Submenu from '@/components/common/Sidebar/Submenu'
type ItemProps = { type ItemProps = {
icon?: IconComponent icon?: IconComponent | string
text?: string text?: string
path: string path: string
children?: ReactNode children?: ReactNode
extend?: ReactNode
end?: boolean end?: boolean
} }
const Item = (props: ItemProps) => { const Item = (props: ItemProps) => {
const [submenuTop, setSubmenuTop] = useState(0) const [submenuTop, setSubmenuTop] = useState(Number.MAX_VALUE)
const [submenuLeft, setSubmenuLeft] = useState(0) const [submenuLeft, setSubmenuLeft] = useState(Number.MAX_VALUE)
const showSubmenu = (e: MouseEvent) => { const showSubmenu = (e: MouseEvent) => {
const parentElement = e.currentTarget.parentElement const parentElement = e.currentTarget.parentElement
@@ -41,10 +42,21 @@ const Item = (props: ItemProps) => {
isPending ? 'pending' : isActive ? 'active' : '' isPending ? 'pending' : isActive ? 'active' : ''
} }
> >
{props.icon && (
<div className={'icon-box'}> <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> </div>
)}
<span className={'text'}>{props.text}</span> <span className={'text'}>{props.text}</span>
<div className={'extend'}>{props.extend}</div>
</NavLink> </NavLink>
</div> </div>
{props.children && ( {props.children && (

View File

@@ -17,18 +17,18 @@ interface SidebarProps extends PropsWithChildren {
} }
const Sidebar = (props: SidebarProps) => { const Sidebar = (props: SidebarProps) => {
const [hideSidebar, setHideSidebar] = useState(getLocalStorage('HIDE_SIDEBAR') === 'true') const [isHideSidebar, setIsHideSidebar] = useState(getLocalStorage('HIDE_SIDEBAR') === 'true')
const switchSidebar = () => { const switchSidebar = () => {
setLocalStorage('HIDE_SIDEBAR', !hideSidebar ? 'true' : 'false') setLocalStorage('HIDE_SIDEBAR', !isHideSidebar ? 'true' : 'false')
setHideSidebar(!hideSidebar) setIsHideSidebar(!isHideSidebar)
props.onSidebarSwitch?.(hideSidebar) props.onSidebarSwitch?.(isHideSidebar)
} }
return ( return (
<> <>
<div <div
className={`sidebar${hideSidebar ? ' hide' : ''}`} className={`sidebar${isHideSidebar ? ' hide' : ''}`}
style={{ width: props.width ?? 'clamp(180px, 20vw, 240px)' }} style={{ width: props.width ?? 'clamp(180px, 20vw, 240px)' }}
> >
<div className={'title'}> <div className={'title'}>

View File

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

View File

@@ -0,0 +1,27 @@
import { HandleContextInst } from '@/components/dnd/HandleContext'
import Icon from '@ant-design/icons'
import '@/assets/css/components/dnd/drag-handle.scss'
interface DragHandleProps {
padding?: string | number
}
const DragHandle = ({ padding }: DragHandleProps) => {
// eslint-disable-next-line @typescript-eslint/unbound-method
const { attributes, listeners, ref } = useContext(HandleContextInst)
return (
<button
data-component={'component-drag-handle'}
style={{ padding }}
ref={ref}
className={'drag-handle'}
{...attributes}
{...listeners}
>
<Icon component={IconOxygenHandle} />
</button>
)
}
export default DragHandle

View File

@@ -0,0 +1,47 @@
import { CSSProperties, PropsWithChildren } from 'react'
import { useDraggable } from '@dnd-kit/core'
import { HandleContext, HandleContextInst } from '@/components/dnd/HandleContext'
interface DraggableProps extends PropsWithChildren {
id: string
data: ToolMenuItem
}
const Draggable = ({ id, data, children }: DraggableProps) => {
const {
attributes,
isDragging,
listeners,
setNodeRef: draggableRef,
setActivatorNodeRef,
transform
} = useDraggable({
id,
data
})
const context = useMemo<HandleContext>(
() => ({
attributes,
listeners,
ref: setActivatorNodeRef
}),
[attributes, listeners, setActivatorNodeRef]
)
const style: CSSProperties | undefined = transform
? {
opacity: isDragging ? 0 : undefined,
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
zIndex: 10000
}
: undefined
return (
<HandleContextInst.Provider value={context}>
<div ref={draggableRef} style={style}>
{children}
</div>
</HandleContextInst.Provider>
)
}
export default Draggable

View File

@@ -0,0 +1,22 @@
import { PropsWithChildren } from 'react'
import { defaultDropAnimationSideEffects, DragOverlay, DropAnimation } from '@dnd-kit/core'
interface DraggableOverlayProps extends PropsWithChildren {
isDelete?: boolean
}
const dropAnimationConfig: DropAnimation = {
sideEffects: defaultDropAnimationSideEffects({
styles: {
active: {
opacity: '0.4'
}
}
})
}
const DraggableOverlay = ({ children }: DraggableOverlayProps) => {
return <DragOverlay dropAnimation={dropAnimationConfig}>{children}</DragOverlay>
}
export default DraggableOverlay

View File

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

View File

@@ -0,0 +1,16 @@
import { DetailedHTMLProps, HTMLAttributes } from 'react'
import { useDroppable } from '@dnd-kit/core'
interface DroppableProps extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
id: string
}
const Droppable = ({ id, ...props }: DroppableProps) => {
const { setNodeRef: droppableRef } = useDroppable({
id
})
return <div {...props} ref={droppableRef} />
}
export default Droppable

View File

@@ -0,0 +1,13 @@
import { DraggableSyntheticListeners } from '@dnd-kit/core'
export interface HandleContext {
attributes: Record<string, any>
listeners: DraggableSyntheticListeners
ref(node: HTMLElement | null): void
}
export const HandleContextInst = createContext<HandleContext>({
attributes: {},
listeners: undefined,
ref() {}
})

View File

@@ -0,0 +1,54 @@
import { CSSProperties, PropsWithChildren } from 'react'
import { useSortable } from '@dnd-kit/sortable'
import { HandleContext, HandleContextInst } from '@/components/dnd/HandleContext'
interface SortableProps extends PropsWithChildren {
id: string
data: ToolMenuItem
isDelete?: boolean
}
const Sortable = ({ id, data, isDelete, children }: SortableProps) => {
const {
attributes,
isDragging,
listeners,
setNodeRef: draggableRef,
setActivatorNodeRef,
transform,
transition
} = useSortable({
id,
data
})
const context = useMemo<HandleContext>(
() => ({
attributes,
listeners,
ref: setActivatorNodeRef
}),
[attributes, listeners, setActivatorNodeRef]
)
const style: CSSProperties | undefined = transform
? {
opacity: isDragging ? 0.4 : undefined,
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
zIndex: 10000,
transition
}
: undefined
return (
<HandleContextInst.Provider value={context}>
<div
ref={draggableRef}
style={style}
className={isDragging && isDelete ? 'delete' : undefined}
>
{children}
</div>
</HandleContextInst.Provider>
)
}
export default Sortable

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,198 @@
import { DetailedHTMLProps, HTMLAttributes, MouseEvent } from 'react'
import VanillaTilt, { TiltOptions } from 'vanilla-tilt'
import Icon from '@ant-design/icons'
import '@/assets/css/components/tools/local-card.scss'
import { COLOR_BACKGROUND, COLOR_MAIN } from '@/constants/common.constants'
import { checkDesktop, omitText } from '@/util/common'
import { getAndroidUrl, navigateToStore, navigateToView } from '@/util/navigation'
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[]
}
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,
...props
}: StoreCardProps) => {
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.confirm({
centered: true,
icon: <Icon style={{ color: COLOR_MAIN }} component={IconOxygenInfo} />,
title: 'Android 端',
content: (
<FlexBox className={'android-qrcode'}>
<AntdQRCode value={getAndroidUrl(author.username, toolId)} size={300} />
<AntdTag className={'tag'}>使</AntdTag>
</FlexBox>
),
okText: '确定',
cancelText: '模拟器',
onCancel() {
navigateToView(navigate, author.username, toolId, platform, undefined, true)
}
})
return
}
navigateToView(navigate, author.username, toolId, platform, undefined, true)
}
const handleOnClickAuthor = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
navigateToStore(navigate, author.username)
}
const handleOnAndroidBtnClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
void modal.confirm({
centered: true,
icon: <Icon style={{ color: COLOR_MAIN }} component={IconOxygenInfo} />,
title: 'Android 端',
content: (
<FlexBox className={'android-qrcode'}>
<AntdQRCode value={getAndroidUrl(author.username, toolId)} size={300} />
<AntdTag className={'tag'}>使</AntdTag>
</FlexBox>
),
okText: '确定',
cancelText: '模拟器',
onCancel() {
navigateToView(navigate, author.username, toolId, 'ANDROID', undefined, true)
}
})
}
const handleOnWebBtnClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
navigateToView(navigate, author.username, toolId, 'WEB', undefined, true)
}
return (
<>
<Draggable
id={`${author.username}:${toolId}:local:${platform}`}
data={{
icon,
toolName,
toolId,
authorUsername: author.username,
ver: 'local',
platform: platform
}}
>
<Card
data-component={'component-local-card'}
style={{ overflow: 'visible', ...style }}
ref={cardRef}
{...props}
onClick={handleCardOnClick}
>
<FlexBox className={'local-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>
)}
<DragHandle />
</div>
</div>
<div className={'icon'}>
<img src={`data:image/svg+xml;base64,${icon}`} alt={'Icon'} />
</div>
<div className={'info'}>
<div className={'tool-name'}>{toolName}</div>
<div className={'tool-id'}>{`ID: ${toolId}`}</div>
{toolDesc && (
<div
className={'tool-desc'}
title={toolDesc}
>{`简介:${omitText(toolDesc, 18)}`}</div>
)}
</div>
{showAuthor && (
<div className={'author'} onClick={handleOnClickAuthor}>
<div className={'avatar'}>
<AntdAvatar
src={
<AntdImage
preview={false}
src={`data:image/png;base64,${author.userInfo.avatar}`}
alt={'Avatar'}
/>
}
style={{ background: COLOR_BACKGROUND }}
/>
</div>
<div className={'author-name'}>{author.userInfo.nickname}</div>
</div>
)}
</FlexBox>
</Card>
</Draggable>
{contextHolder}
</>
)
}
export default StoreCard

View File

@@ -0,0 +1,115 @@
import { DetailedHTMLProps, HTMLAttributes } from 'react'
import VanillaTilt, { TiltOptions } from 'vanilla-tilt'
import '@/assets/css/components/tools/repository-card.scss'
import Card from '@/components/common/Card'
import FlexBox from '@/components/common/FlexBox'
import Draggable from '@/components/dnd/Draggable'
import DragHandle from '@/components/dnd/DragHandle'
interface RepositoryCardProps
extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
icon: string
toolName: string
toolId: string
ver: string
platform: Platform
options?: TiltOptions
onOpen?: () => void
onEdit?: () => void
onSource?: () => void
onPublish?: () => void
onCancelReview?: () => void
onDelete?: () => void
}
const RepositoryCard = ({
style,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
ref,
icon,
toolName,
toolId,
ver,
platform,
options = {
reverse: true,
max: 8,
glare: true,
['max-glare']: 0.3,
scale: 1.03
},
onOpen,
onEdit,
onSource,
onPublish,
onCancelReview,
onDelete,
children,
...props
}: RepositoryCardProps) => {
const cardRef = useRef<HTMLDivElement>(null)
useEffect(() => {
cardRef.current && VanillaTilt.init(cardRef.current, options)
}, [options])
return (
<Draggable
id={`!:${toolId}:${ver}:${platform}`}
data={{ icon, toolName, toolId, authorUsername: '!', ver, platform }}
>
<Card
data-component={'component-repository-card'}
style={{ overflow: 'visible', ...style }}
ref={cardRef}
{...props}
>
<FlexBox className={'repository-card'}>
<div className={'header'}>
{children}
<DragHandle />
</div>
<div className={'icon'}>
<img src={`data:image/svg+xml;base64,${icon}`} alt={'Icon'} />
</div>
<div className={'info'}>
<div className={'tool-name'}>{toolName}</div>
<div className={'tool-id'}>{`ID: ${toolId}`}</div>
</div>
<div className={'operation'}>
{onOpen && (
<AntdButton onClick={onOpen} size={'small'} type={'primary'}>
</AntdButton>
)}
{onEdit && onPublish && (
<div className={'edit'}>
<AntdButton.Group size={'small'}>
<AntdButton onClick={onEdit}></AntdButton>
<AntdButton onClick={onPublish}></AntdButton>
</AntdButton.Group>
</div>
)}
{onSource && (
<AntdButton size={'small'} onClick={onSource}>
</AntdButton>
)}
{onCancelReview && (
<AntdButton size={'small'} onClick={onCancelReview}>
</AntdButton>
)}
{onDelete && (
<AntdButton size={'small'} danger onClick={onDelete}>
</AntdButton>
)}
</div>
</FlexBox>
</Card>
</Draggable>
)
}
export default RepositoryCard

View File

@@ -0,0 +1,407 @@
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,
DATABASE_SELECT_SUCCESS
} from '@/constants/common.constants'
import { checkDesktop, omitText } from '@/util/common'
import { getLoginStatus, getUserId } from '@/util/auth'
import {
getAndroidUrl,
navigateToLogin,
navigateToSource,
navigateToStore,
navigateToView
} from '@/util/navigation'
import {
l_tool_get,
l_tool_install,
r_tool_add_favorite,
r_tool_detail,
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[]
vers: Record<Platform, string>
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,
vers,
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('')
const [isInstalling, setIsInstalling] = useState(false)
const [isInstalled, setIsInstalled] = useState(true)
const [isAvailableUpdate, setIsAvailableUpdate] = useState(false)
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({
centered: true,
icon: <Icon style={{ color: COLOR_MAIN }} component={IconOxygenInfo} />,
title: 'Android 端',
content: (
<FlexBox className={'android-qrcode'}>
<AntdQRCode value={getAndroidUrl(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 handleOnInstallBtnClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
if (isInstalling) {
return
}
setIsInstalling(true)
void message.loading({
content: isAvailableUpdate ? '更新中' : '安装中',
key: 'INSTALLING',
duration: 0
})
const newTools = {} as Record<Platform, ToolVo>
const flags: boolean[] = []
supportPlatform.forEach((platform) => {
void r_tool_detail(author.username, toolId, 'latest', platform)
.then((res) => {
const response = res.data
switch (response.code) {
case DATABASE_SELECT_SUCCESS:
newTools[platform] = response.data!
flags.push(true)
break
default:
flags.push(false)
}
})
.catch(() => {
flags.push(false)
})
.finally(() => {
message.destroy('INSTALLING')
setIsInstalling(false)
if (flags.length !== supportPlatform.length) {
return
}
if (flags.every((item) => item)) {
void l_tool_install({ [`${author.username}:${toolId}`]: newTools }).then(
() => {
void message.success(isAvailableUpdate ? '更新成功' : '安装成功')
setIsInstalled(true)
setIsAvailableUpdate(false)
}
)
} else {
void message.error(
isAvailableUpdate ? '更新失败,请稍后重试' : '安装失败,请稍后重试'
)
}
})
})
}
const handleOnAndroidBtnClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
void modal.confirm({
centered: true,
icon: <Icon style={{ color: COLOR_MAIN }} component={IconOxygenInfo} />,
title: 'Android 端',
content: (
<FlexBox className={'android-qrcode'}>
<AntdQRCode value={getAndroidUrl(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(
`${import.meta.env.VITE_DESKTOP_PROTOCOL}://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')
}
useEffect(() => {
void l_tool_get().then((value) => {
const tools = value[`${author.username}:${toolId}`]
if (!tools) {
setIsInstalled(false)
return
}
setIsInstalled(true)
if (
Object.keys(tools).length !== supportPlatform.length ||
!supportPlatform.every((platform) => Object.keys(tools).includes(platform))
) {
setIsAvailableUpdate(true)
return
}
if (supportPlatform.some((platform) => vers[platform] !== tools[platform].ver)) {
setIsAvailableUpdate(true)
return
}
})
}, [])
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'}>
{(!isInstalled || isAvailableUpdate) && (
<AntdTooltip title={isAvailableUpdate ? '更新' : '安装'}>
<Icon
component={IconOxygenDownload}
onClick={handleOnInstallBtnClick}
disabled={isInstalling}
/>
</AntdTooltip>
)}
{platform !== 'ANDROID' && supportPlatform.includes('ANDROID') && (
<AntdTooltip title={'Android 端'}>
<Icon
component={IconOxygenMobile}
onClick={handleOnAndroidBtnClick}
/>
</AntdTooltip>
)}
{platform === 'DESKTOP' && supportPlatform.includes('WEB') && (
<AntdTooltip title={'Web 端'}>
<Icon
component={IconOxygenBrowser}
onClick={handleOnWebBtnClick}
/>
</AntdTooltip>
)}
{platform === 'WEB' && supportPlatform.includes('DESKTOP') && (
<AntdTooltip title={'桌面端'}>
<Icon
component={IconOxygenDesktop}
onClick={handleOnDesktopBtnClick}
/>
</AntdTooltip>
)}
<AntdTooltip title={'源码'}>
<Icon
component={IconOxygenCode}
onClick={handleOnSourceBtnClick}
/>
</AntdTooltip>
{author.id !== userId && (
<AntdTooltip title={favorite_ ? '取消收藏' : '收藏'}>
<Icon
component={
favorite_ ? IconOxygenStarFilled : IconOxygenStar
}
style={{
color: favorite_ ? COLOR_PRODUCTION : undefined
}}
onClick={handleOnStarBtnClick}
/>
</AntdTooltip>
)}
<DragHandle />
</div>
</div>
<div className={'icon'}>
<img src={`data:image/svg+xml;base64,${icon}`} alt={'Icon'} />
</div>
<div className={'info'}>
<div className={'tool-name'}>{toolName}</div>
<div className={'tool-id'}>{`ID: ${toolId}`}</div>
{toolDesc && (
<div
className={'tool-desc'}
title={toolDesc}
>{`简介:${omitText(toolDesc, 18)}`}</div>
)}
</div>
{showAuthor && (
<div className={'author'} onClick={handleOnClickAuthor}>
<div className={'avatar'}>
<AntdAvatar
src={
<AntdImage
preview={false}
src={`data:image/png;base64,${author.userInfo.avatar}`}
alt={'Avatar'}
/>
}
style={{ background: COLOR_BACKGROUND }}
/>
</div>
<div className={'author-name'}>{author.userInfo.nickname}</div>
</div>
)}
</FlexBox>
</Card>
</Draggable>
{contextHolder}
</>
)
}
export default StoreCard

View File

@@ -1,6 +1,7 @@
export const PRODUCTION_NAME = 'Oxygen Toolbox' export const PRODUCTION_NAME = 'Oxygen Toolbox'
export const STORAGE_TOKEN_KEY = 'JWT_TOKEN' export const STORAGE_TOKEN_KEY = 'JWT_TOKEN'
export const STORAGE_USER_INFO_KEY = 'USER_INFO' 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_ORIGIN = 'white'
export const COLOR_PRODUCTION = '#4E47BB' export const COLOR_PRODUCTION = '#4E47BB'
export const COLOR_MAIN = COLOR_PRODUCTION export const COLOR_MAIN = COLOR_PRODUCTION

View File

@@ -30,6 +30,7 @@ export const URL_SYS_STATISTICS_ACTIVE = `${URL_SYS_STATISTICS}/active`
export const URL_SYS_TOOL = '/system/tool' export const URL_SYS_TOOL = '/system/tool'
export const URL_SYS_TOOL_CATEGORY = `${URL_SYS_TOOL}/category` export const URL_SYS_TOOL_CATEGORY = `${URL_SYS_TOOL}/category`
export const URL_SYS_TOOL_BASE = `${URL_SYS_TOOL}/base` export const URL_SYS_TOOL_BASE = `${URL_SYS_TOOL}/base`
export const URL_SYS_TOOL_BASE_LIST = `${URL_SYS_TOOL_BASE}/list`
export const URL_SYS_TOOL_TEMPLATE = `${URL_SYS_TOOL}/template` export const URL_SYS_TOOL_TEMPLATE = `${URL_SYS_TOOL}/template`
export const URL_TOOL = '/tool' export const URL_TOOL = '/tool'
@@ -37,6 +38,7 @@ export const URL_TOOL_STORE = `${URL_TOOL}/store`
export const URL_TOOL_TEMPLATE = `${URL_TOOL}/template` export const URL_TOOL_TEMPLATE = `${URL_TOOL}/template`
export const URL_TOOL_CATEGORY = `${URL_TOOL}/category` export const URL_TOOL_CATEGORY = `${URL_TOOL}/category`
export const URL_TOOL_DETAIL = `${URL_TOOL}/detail` 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 = '/api/v1'
export const URL_API_V1_AVATAR_RANDOM_BASE64 = `${URL_API_V1}/avatar/base64` export const URL_API_V1_AVATAR_RANDOM_BASE64 = `${URL_API_V1}/avatar/base64`

16
src/renderer/src/electron.d.ts vendored Normal file
View File

@@ -0,0 +1,16 @@
import { ElectronAPI } from '@electron-toolkit/preload'
import { Notification } from 'electron'
declare global {
type _ElectronAPI = ElectronAPI
class _Notification extends Notification {}
interface API {
installTool: (
newTools: Record<string, Record<Platform, ToolVo>>
) => Promise<Record<string, Record<Platform, ToolVo>>>
getInstalledTool: () => Promise<Record<string, Record<Platform, ToolVo>>>
}
}

View File

@@ -1,7 +1,14 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
/// <reference types="./electron" />
/// <reference types="./ant-design" /> /// <reference types="./ant-design" />
type Platform = 'WEB' | 'DESKTOP' | 'ANDROID'
interface ImportMetaEnv { interface ImportMetaEnv {
readonly VITE_PLATFORM: Platform
readonly VITE_DESKTOP_PROTOCOL: string
readonly VITE_APP_PROTOCOL: string
readonly VITE_UI_URL: string
readonly VITE_API_URL: string readonly VITE_API_URL: string
readonly VITE_API_TOKEN_URL: string readonly VITE_API_TOKEN_URL: string
readonly VITE_TURNSTILE_SITE_KEY: string readonly VITE_TURNSTILE_SITE_KEY: string
@@ -11,6 +18,12 @@ interface ImportMeta {
readonly env: ImportMetaEnv readonly env: ImportMetaEnv
} }
interface Window {
electronAPI: _ElectronAPI
Notification: typeof _Notification
api: API
}
interface RouteJsonObject { interface RouteJsonObject {
path: string path: string
absolutePath: string absolutePath: string
@@ -545,6 +558,7 @@ interface ToolBaseVo {
name: string name: string
source: ToolDataVo source: ToolDataVo
dist: ToolDataVo dist: ToolDataVo
platform: Platform
compiled: boolean compiled: boolean
createTime: string createTime: string
updateTime: string updateTime: string
@@ -555,6 +569,7 @@ interface ToolBaseAddEditParam {
name?: string name?: string
source?: string source?: string
dist?: string dist?: string
platform?: Platform
} }
interface ToolTemplateVo { interface ToolTemplateVo {
@@ -562,6 +577,7 @@ interface ToolTemplateVo {
name: string name: string
baseId: string baseId: string
source: ToolDataVo source: ToolDataVo
platform: Platform
entryPoint: string entryPoint: string
enable: boolean enable: boolean
createTime: string createTime: string
@@ -574,6 +590,7 @@ interface ToolTemplateAddEditParam {
name?: string name?: string
baseId?: string baseId?: string
source?: string source?: string
platform?: Platform
entryPoint?: string entryPoint?: string
enable?: boolean enable?: boolean
} }
@@ -583,6 +600,7 @@ interface ToolVo {
name: string name: string
toolId: string toolId: string
icon: string icon: string
platform: Platform
description: string description: string
base: ToolBaseVo base: ToolBaseVo
author: UserWithInfoVo author: UserWithInfoVo
@@ -596,12 +614,14 @@ interface ToolVo {
review: 'NONE' | 'PROCESSING' | 'PASS' | 'REJECT' review: 'NONE' | 'PROCESSING' | 'PASS' | 'REJECT'
createTime: string createTime: string
updateTime: string updateTime: string
favorite: boolean
} }
interface ToolCreateParam { interface ToolCreateParam {
name: string name: string
toolId: string toolId: string
icon: string icon: string
platform: Platform
description: string description: string
ver: string ver: string
templateId: string templateId: string
@@ -612,6 +632,7 @@ interface ToolCreateParam {
interface ToolUpgradeParam { interface ToolUpgradeParam {
toolId: string toolId: string
ver: string ver: string
platform: Platform
} }
interface ToolUpdateParam { interface ToolUpdateParam {
@@ -637,3 +658,18 @@ interface ToolManagementPassParam {
interface ToolStoreGetParam extends PageParam { interface ToolStoreGetParam extends PageParam {
searchValue?: string searchValue?: string
} }
interface ToolFavoriteAddRemoveParam {
authorId: string
toolId: string
platform: Platform
}
interface ToolMenuItem {
icon: string
toolName: string
toolId: string
authorUsername: string
ver: string
platform: Platform
}

View File

@@ -10,7 +10,10 @@ createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<AntdConfigProvider <AntdConfigProvider
theme={{ theme={{
token: { colorPrimary: COLOR_MAIN, colorLinkHover: COLOR_MAIN }, token: {
colorPrimary: COLOR_MAIN,
colorLinkHover: COLOR_MAIN
},
components: { components: {
Tree: { Tree: {
colorBgContainer: 'transparent' colorBgContainer: 'transparent'

View File

@@ -8,6 +8,7 @@ import {
PERMISSION_USER_NOT_FOUND, PERMISSION_USER_NOT_FOUND,
SYSTEM_INVALID_CAPTCHA_CODE SYSTEM_INVALID_CAPTCHA_CODE
} from '@/constants/common.constants' } from '@/constants/common.constants'
import { navigateToLogin } from '@/util/navigation'
import { r_auth_forget, r_auth_retrieve } from '@/services/auth' import { r_auth_forget, r_auth_retrieve } from '@/services/auth'
import FitCenter from '@/components/common/FitCenter' import FitCenter from '@/components/common/FitCenter'
import FlexBox from '@/components/common/FlexBox' import FlexBox from '@/components/common/FlexBox'
@@ -155,8 +156,8 @@ const Forget = () => {
> >
<AntdInput <AntdInput
prefix={<Icon component={IconOxygenEmail} />} prefix={<Icon component={IconOxygenEmail} />}
placeholder={'邮箱'}
disabled={isSending} disabled={isSending}
placeholder={'邮箱'}
/> />
</AntdForm.Item> </AntdForm.Item>
<AntdForm.Item> <AntdForm.Item>
@@ -199,6 +200,7 @@ const Forget = () => {
name={'password'} name={'password'}
rules={[ rules={[
{ required: true, message: '请输入密码' }, { required: true, message: '请输入密码' },
{ whitespace: true, message: '密码不能为空字符' },
{ min: 10, message: '密码至少为10位' }, { min: 10, message: '密码至少为10位' },
{ max: 30, message: '密码最多为30位' } { max: 30, message: '密码最多为30位' }
]} ]}
@@ -208,8 +210,8 @@ const Forget = () => {
addonBefore={ addonBefore={
<span>&nbsp;&nbsp;&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;&nbsp;&nbsp;</span>
} }
placeholder={'密码'}
disabled={isChanging} disabled={isChanging}
placeholder={'密码'}
/> />
</AntdForm.Item> </AntdForm.Item>
<AntdForm.Item <AntdForm.Item
@@ -234,8 +236,8 @@ const Forget = () => {
<AntdInput.Password <AntdInput.Password
id={'forget-password-confirm'} id={'forget-password-confirm'}
addonBefore={'确认密码'} addonBefore={'确认密码'}
placeholder={'确认密码'}
disabled={isChanging} disabled={isChanging}
placeholder={'确认密码'}
/> />
</AntdForm.Item> </AntdForm.Item>
<AntdForm.Item> <AntdForm.Item>
@@ -271,7 +273,15 @@ const Forget = () => {
<div className={'footer'}> <div className={'footer'}>
<a onClick={() => navigate(`/login`, { replace: true })}></a> <a
onClick={() =>
navigateToLogin(navigate, location.search, undefined, {
replace: true
})
}
>
</a>
</div> </div>
</div> </div>
</FlexBox> </FlexBox>

Some files were not shown because too many files have changed in this diff Show More