Compare commits
72 Commits
dev
...
395c547e85
| Author | SHA1 | Date | |
|---|---|---|---|
|
395c547e85
|
|||
|
43779227c2
|
|||
|
69e572ae23
|
|||
|
df98339838
|
|||
|
7d3ec39da3
|
|||
|
32686848d2
|
|||
|
1df704f65e
|
|||
|
c5637d5e7f
|
|||
|
a7577373ef
|
|||
|
3dc434a6ac
|
|||
|
898075cf6e
|
|||
|
58bb5eb262
|
|||
|
31a458416d
|
|||
|
2b1bd719ad
|
|||
|
606064b1fb
|
|||
|
19113a56af
|
|||
|
6dd1f51c89
|
|||
|
22e2e15db8
|
|||
|
4f55364f65
|
|||
|
d0ddb276c0
|
|||
|
8f22084d10
|
|||
|
30ec21a71e
|
|||
|
513f66418a
|
|||
|
580249a85c
|
|||
|
1ef2db5f47
|
|||
|
d677aeed07
|
|||
|
361a5c923d
|
|||
|
c87e697ea6
|
|||
|
058984ad85
|
|||
|
3ddd4c88e0
|
|||
|
c64777e759
|
|||
|
d918e527a0
|
|||
|
b51fdb11f2
|
|||
|
6e2012cd7a
|
|||
|
cab0c9d879
|
|||
|
a5ca0be53c
|
|||
|
e5bc568d23
|
|||
|
2176c0ce2a
|
|||
|
ac2e3c8e6e
|
|||
|
63c76440a6
|
|||
|
7f034db314
|
|||
|
d326c8b8e5
|
|||
|
ea68945df1
|
|||
|
59eef73895
|
|||
|
2f9c01981b
|
|||
|
5250b662f5
|
|||
|
77166831bf
|
|||
|
9152ec8dfb
|
|||
|
cfddc85302
|
|||
|
76470d741f
|
|||
|
62a70f92ae
|
|||
|
5a12020d69
|
|||
|
71faca682e
|
|||
|
cd1802b5d6
|
|||
|
0aad1ac966
|
|||
|
e209bea2f3
|
|||
|
c65c548ecc
|
|||
|
9b96c7dd62
|
|||
|
a2d0afb38c
|
|||
|
48c9ec148a
|
|||
|
07602bebc8
|
|||
|
5c2a77db30
|
|||
|
7d5265ecda
|
|||
|
058746702d
|
|||
|
84d2e6c3f8
|
|||
|
61d3bb21ad
|
|||
|
8730513340
|
|||
|
6f53a867c3
|
|||
|
c098368a79
|
|||
|
de3d1831d0
|
|||
|
715806b85d
|
|||
|
0b314e4ea9
|
@@ -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
|
||||
@@ -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_TURNSTILE_SITE_KEY=${TURNSTILE_SITE_KEY}
|
||||
@@ -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_TOKEN_URL=${VITE_API_URL}/token
|
||||
VITE_TURNSTILE_SITE_KEY=${TURNSTILE_SITE_KEY}
|
||||
@@ -1,4 +1,8 @@
|
||||
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_TOKEN_URL=${VITE_API_URL}/token
|
||||
VITE_TURNSTILE_SITE_KEY=${TURNSTILE_SITE_KEY}
|
||||
@@ -34,6 +34,7 @@ module.exports = {
|
||||
'warn',
|
||||
{ allowConstantExport: true }
|
||||
],
|
||||
'@typescript-eslint/no-non-null-assertion': 'off'
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
}
|
||||
}
|
||||
|
||||
BIN
build/icon.icns
BIN
build/icon.ico
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 264 KiB |
BIN
build/icon.png
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 978 B After Width: | Height: | Size: 978 B |
@@ -149,7 +149,10 @@ const matchComponents: IMatcher[] = [
|
||||
pattern: /^Mentions/,
|
||||
styleDir: 'mentions'
|
||||
},
|
||||
|
||||
{
|
||||
pattern: /^QRCode/,
|
||||
styleDir: 'qr-code'
|
||||
},
|
||||
{
|
||||
pattern: /^Step/,
|
||||
styleDir: 'steps'
|
||||
@@ -337,6 +340,7 @@ const primitiveNames = [
|
||||
'Rate',
|
||||
'Result',
|
||||
'Row',
|
||||
'QRCode',
|
||||
'Select',
|
||||
'SelectOptGroup',
|
||||
'SelectOption',
|
||||
@@ -417,7 +421,6 @@ export const AntDesignResolver = (options: AntDesignResolverOptions = {}): Compo
|
||||
sideEffects: getSideEffects(importName, options)
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
appId: com.electron.app
|
||||
productName: oxygen-desktop
|
||||
productName: OxygenDesktop
|
||||
copyright: Copyright © 2017-2024 FatWeb. All rights reserved.
|
||||
appId: top.fatweb.oxygen.desktop
|
||||
artifactName: ${name}-${version}-${os}-${arch}.${ext}
|
||||
directories:
|
||||
buildResources: build
|
||||
files:
|
||||
@@ -8,35 +10,78 @@ files:
|
||||
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||
- '!{tsconfig.json,tsconfig.node.json}'
|
||||
asarUnpack:
|
||||
- resources/**
|
||||
win:
|
||||
executableName: oxygen-desktop
|
||||
target:
|
||||
- target: nsis
|
||||
arch:
|
||||
- x64
|
||||
- ia32
|
||||
- arm64
|
||||
- target: msi
|
||||
arch:
|
||||
- x64
|
||||
- ia32
|
||||
- arm64
|
||||
- target: zip
|
||||
arch:
|
||||
- x64
|
||||
- ia32
|
||||
- arm64
|
||||
nsis:
|
||||
artifactName: ${name}-${version}-setup.${ext}
|
||||
shortcutName: ${productName}
|
||||
uninstallDisplayName: ${productName}
|
||||
createDesktopShortcut: always
|
||||
oneClick: false
|
||||
allowToChangeInstallationDirectory: true
|
||||
artifactName: ${name}-${version}-${os}-${arch}-setup.${ext}
|
||||
shortcutName: Oxygen Desktop
|
||||
createDesktopShortcut: true
|
||||
msi:
|
||||
oneClick: false
|
||||
shortcutName: Oxygen Desktop
|
||||
mac:
|
||||
category: public.app-category.productivity
|
||||
target:
|
||||
- default
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
darkModeSupport: true
|
||||
extendInfo:
|
||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||
NSCameraUsageDescription: Application requests access to the device's camera.
|
||||
NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||
NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||
NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||
notarize: false
|
||||
dmg:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
linux:
|
||||
target:
|
||||
- AppImage
|
||||
- snap
|
||||
- deb
|
||||
maintainer: electronjs.org
|
||||
- target: AppImage
|
||||
arch:
|
||||
- x64
|
||||
- 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
|
||||
appImage:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
desktop:
|
||||
Name: Oxygen Desktop
|
||||
Comment: Oxygen Toolbox multi-platform desktop version
|
||||
npmRebuild: false
|
||||
publish:
|
||||
provider: generic
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { resolve } from 'path'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
@@ -10,7 +10,7 @@ import { FileSystemIconLoader } from 'unplugin-icons/loaders'
|
||||
|
||||
export default defineConfig({
|
||||
main: {
|
||||
plugins: [externalizeDepsPlugin()]
|
||||
plugins: [externalizeDepsPlugin({ exclude: ['electron-store'] })]
|
||||
},
|
||||
preload: {
|
||||
plugins: [externalizeDepsPlugin()]
|
||||
@@ -82,7 +82,7 @@ export default defineConfig({
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve('src/renderer/src')
|
||||
'@': fileURLToPath(new URL('./src/renderer/src', import.meta.url))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
5471
package-lock.json
generated
110
package.json
@@ -1,87 +1,101 @@
|
||||
{
|
||||
"name": "oxygen-desktop",
|
||||
"version": "1.0.0",
|
||||
"description": "An Electron application with React and TypeScript",
|
||||
"private": true,
|
||||
"version": "1.0.0-SNAPSHOT",
|
||||
"description": "Oxygen Toolbox multi-platform desktop version",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "example.com",
|
||||
"homepage": "https://electron-vite.org",
|
||||
"author": {
|
||||
"name": "FatttSnake",
|
||||
"email": "fatttsnake@fatweb.top",
|
||||
"url": "https://fatweb.top"
|
||||
},
|
||||
"homepage": "https://tool.fatweb.top",
|
||||
"scripts": {
|
||||
"dev": "electron-vite dev",
|
||||
"dev": "electron-vite dev --sourcemap --remote-debugging-port=9000",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
||||
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||
"typecheck": "tsc",
|
||||
"clean": "rimraf out dist .eslintrc-auto-import.json src/renderer/auto-imports.d.ts",
|
||||
"start": "electron-vite preview",
|
||||
"build": "electron-vite build && npm run typecheck",
|
||||
"build-test": "electron-vite build --mode testing && npm run typecheck",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"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:mac": "electron-vite build && electron-builder --mac",
|
||||
"build:linux": "electron-vite build && electron-builder --linux"
|
||||
"build-test:win": "npm run build-test && electron-builder --win",
|
||||
"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": {
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
"@electron-toolkit/preload": "^3.0.1",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"electron-updater": "^6.1.7"
|
||||
"electron-store": "^9.0.0",
|
||||
"electron-updater": "^6.2.1"
|
||||
},
|
||||
"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-ts": "^1.0.1",
|
||||
"@electron-toolkit/eslint-config-ts": "^2.0.0",
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@marsidev/react-turnstile": "^0.5.3",
|
||||
"@marsidev/react-turnstile": "^0.7.1",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@svgr/core": "^8.1.0",
|
||||
"@svgr/plugin-jsx": "^8.1.0",
|
||||
"@types/jsdom": "^21.1.6",
|
||||
"@types/lodash": "^4.17.0",
|
||||
"@types/node": "^18.19.9",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@typescript/ata": "^0.9.4",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"antd": "^5.15.2",
|
||||
"axios": "^1.6.7",
|
||||
"dayjs": "^1.11.10",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/lodash": "^4.17.5",
|
||||
"@types/node": "^20.14.2",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.13.0",
|
||||
"@typescript-eslint/parser": "^7.13.0",
|
||||
"@typescript/ata": "^0.9.6",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"antd": "^5.18.1",
|
||||
"axios": "^1.7.2",
|
||||
"custom-protocol-check": "^1.4.0",
|
||||
"dayjs": "^1.11.11",
|
||||
"echarts": "^5.5.0",
|
||||
"electron": "^28.2.0",
|
||||
"electron-builder": "^24.9.1",
|
||||
"electron-vite": "^2.0.0",
|
||||
"esbuild-wasm": "^0.20.1",
|
||||
"eslint": "^8.56.0",
|
||||
"electron": "^31.0.1",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-vite": "^2.2.0",
|
||||
"esbuild-wasm": "^0.21.5",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-love": "^52.0.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard-with-typescript": "^43.0.1",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"eslint-plugin-promise": "^6.2.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.7",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fflate": "^0.8.2",
|
||||
"jsdom": "^24.0.0",
|
||||
"jsdom": "^24.1.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"match-sorter": "^6.3.4",
|
||||
"moment": "^2.30.1",
|
||||
"monaco-editor": "^0.47.0",
|
||||
"monaco-editor": "^0.49.0",
|
||||
"monaco-jsx-syntax-highlight": "^1.2.0",
|
||||
"prettier": "^3.2.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"prettier": "^3.3.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-router": "^6.22.3",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"sass": "^1.71.1",
|
||||
"react-router": "^6.23.1",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"sass": "^1.77.5",
|
||||
"size-sensor": "^1.0.2",
|
||||
"stylelint-config-prettier": "^9.0.5",
|
||||
"typescript": "^5.3.3",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
"unplugin-icons": "^0.18.5",
|
||||
"typescript": "^5.4.5",
|
||||
"unplugin-auto-import": "^0.17.6",
|
||||
"unplugin-icons": "^0.19.0",
|
||||
"vanilla-tilt": "^1.8.1",
|
||||
"vite": "^5.0.12"
|
||||
"vite": "^5.2.13"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 260 KiB |
|
Before Width: | Height: | Size: 43 KiB |
21
src/main/dataProcess.ts
Normal 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
@@ -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
|
||||
}
|
||||
@@ -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 { 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.
|
||||
const mainWindow = new BrowserWindow({
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 900,
|
||||
height: 670,
|
||||
show: false,
|
||||
@@ -32,7 +91,10 @@ function createWindow(): void {
|
||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
void mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||||
} 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.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
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
|
||||
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
|
||||
// code. You can also put them in separate files and require them here.
|
||||
import './dataProcess'
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
import { contextBridge, Notification } from 'electron'
|
||||
import { contextBridge, Notification, ipcRenderer } from 'electron'
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
|
||||
// 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
|
||||
// renderer only if context isolation is enabled, otherwise
|
||||
// just add to the DOM global.
|
||||
if (process.contextIsolated) {
|
||||
try {
|
||||
contextBridge.exposeInMainWorld('electron', electronAPI)
|
||||
contextBridge.exposeInMainWorld('electronAPI', electronAPI)
|
||||
contextBridge.exposeInMainWorld('api', api)
|
||||
contextBridge.exposeInMainWorld('notification', Notification)
|
||||
contextBridge.exposeInMainWorld('Notification', Notification)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
} else {
|
||||
// @ts-expect-error (define in dts)
|
||||
window.electron = electronAPI
|
||||
window.electronAPI = electronAPI
|
||||
// @ts-expect-error (define in dts)
|
||||
window.api = api
|
||||
// @ts-expect-error (define in dts)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getRedirectUrl } from '@/util/route'
|
||||
import { getLoginStatus, getVerifyStatus_async } from '@/util/auth'
|
||||
|
||||
const AuthRoute = () => {
|
||||
const navigate = useNavigate()
|
||||
const [searchParams] = useSearchParams()
|
||||
const matches = useMatches()
|
||||
const lastMatch = matches.reduce((_, second) => second)
|
||||
@@ -12,6 +13,12 @@ const AuthRoute = () => {
|
||||
const isLogin = getLoginStatus()
|
||||
const isVerify = getVerifyStatus_async()
|
||||
|
||||
useEffect(() => {
|
||||
window.electronAPI.ipcRenderer.on('open-url', (_, url: string) => {
|
||||
navigate(url)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return useMemo(() => {
|
||||
document.title = `${handle?.titlePrefix ?? ''}${
|
||||
handle?.title ? handle?.title : PRODUCTION_NAME
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
-ms-overflow-style: none;
|
||||
|
||||
.hide-scrollbar-content {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +67,6 @@
|
||||
}
|
||||
|
||||
.vertical-scrollbar {
|
||||
padding: 12px 2px;
|
||||
height: 100%;
|
||||
left: 100%;
|
||||
top: 0;
|
||||
@@ -80,7 +78,6 @@
|
||||
}
|
||||
|
||||
.horizontal-scrollbar {
|
||||
padding: 4px 12px;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 100%;
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
font-size: constants.$SIZE_ICON_SM;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
span {
|
||||
transform: rotateZ(180deg);
|
||||
transition: all .3s;
|
||||
@@ -55,14 +56,16 @@
|
||||
.scroll {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ul {
|
||||
> li {
|
||||
> li, > div > li {
|
||||
padding: 2px 14px;
|
||||
|
||||
&.item {
|
||||
position: relative;
|
||||
margin: 4px 14px;
|
||||
font-size: 1.4em;
|
||||
font-size: 1rem;
|
||||
|
||||
> .menu-bt {
|
||||
border-radius: 8px;
|
||||
@@ -78,6 +81,10 @@
|
||||
height: 40px;
|
||||
font-size: constants.$SIZE_ICON_SM;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -86,15 +93,24 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
transition: all 0.2s;
|
||||
background-color: constants.$origin-color;
|
||||
|
||||
.text {
|
||||
flex: 1;
|
||||
padding-left: 8px;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: constants.$origin-color;
|
||||
background-color: constants.$main-color !important;
|
||||
background-color: constants.$main-color;
|
||||
|
||||
img {
|
||||
filter: drop-shadow(1000px 0 0 constants.$origin-color);
|
||||
transform: translate(-1000px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,7 +118,7 @@
|
||||
.submenu {
|
||||
visibility: hidden;
|
||||
position: fixed;
|
||||
padding-left: 20px;
|
||||
padding-left: 10px;
|
||||
z-index: 10000;
|
||||
animation: 0.1s ease forwards;
|
||||
@include mixins.unique-keyframes {
|
||||
@@ -123,24 +139,30 @@
|
||||
padding: 10px 10px;
|
||||
background-color: constants.$origin-color;
|
||||
border-radius: 8px;
|
||||
box-shadow: 2px 2px 10px 0 rgba(0,0,0,0.1);
|
||||
|
||||
.item {
|
||||
border-radius: 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: 8px 16px;
|
||||
transition: all 0.2s;
|
||||
|
||||
.text {
|
||||
width: unset;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: constants.$origin-color;
|
||||
background-color: constants.$main-color !important;
|
||||
background-color: constants.$main-color;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover a {
|
||||
&:hover a:not(.active) {
|
||||
background-color: constants.$background-color;
|
||||
}
|
||||
}
|
||||
@@ -149,7 +171,7 @@
|
||||
|
||||
&:hover {
|
||||
> .menu-bt {
|
||||
a {
|
||||
a:not(.active) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-bt {
|
||||
.text {
|
||||
.text, .extend {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -272,6 +311,7 @@
|
||||
|
||||
.footer {
|
||||
position: relative;
|
||||
|
||||
.text {
|
||||
display: none;
|
||||
}
|
||||
@@ -291,6 +331,7 @@
|
||||
|
||||
.icon-exit {
|
||||
padding: 4px 8px;
|
||||
|
||||
&:hover {
|
||||
border-radius: 8px;
|
||||
background-color: constants.$background-color;
|
||||
|
||||
28
src/renderer/src/assets/css/components/common/url-card.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
@use '@/assets/css/constants' as constants;
|
||||
|
||||
[data-component=component-url-card] {
|
||||
cursor: pointer;
|
||||
|
||||
.url-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-top: 80px;
|
||||
text-align: center;
|
||||
gap: 42px;
|
||||
|
||||
> * {
|
||||
flex: 0 0 auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: constants.$production-color;
|
||||
font-size: constants.$SIZE_ICON_XL;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-weight: bolder;
|
||||
font-size: 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
[data-component=component-drag-handle] {
|
||||
background-color: transparent;
|
||||
color: unset;
|
||||
cursor: grab;
|
||||
}
|
||||
30
src/renderer/src/assets/css/components/dnd/drop-mask.scss
Normal file
@@ -0,0 +1,30 @@
|
||||
@use "@/assets/css/constants" as constants;
|
||||
|
||||
[data-component=component-drop-mask] {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: {
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
};
|
||||
background-color: constants.$origin-color;
|
||||
|
||||
.drop-mask-border {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: {
|
||||
width: 2px;
|
||||
color: constants.$font-secondary-color;
|
||||
style: dashed;
|
||||
radius: 8px;
|
||||
};
|
||||
font-size: 1.8em;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
110
src/renderer/src/assets/css/components/tools/local-card.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
111
src/renderer/src/assets/css/components/tools/store-card.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,40 +2,16 @@
|
||||
|
||||
[data-component=system] {
|
||||
.root-content {
|
||||
padding: 30px;
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
|
||||
> .card-box {
|
||||
width: 200px;
|
||||
height: 360px;
|
||||
height: 320px;
|
||||
flex: 0 0 auto;
|
||||
overflow: hidden !important;
|
||||
cursor: pointer;
|
||||
|
||||
.common-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-top: 100px;
|
||||
text-align: center;
|
||||
gap: 42px;
|
||||
|
||||
> * {
|
||||
flex: 0 0 auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: constants.$production-color;
|
||||
font-size: constants.$SIZE_ICON_XL;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-weight: bolder;
|
||||
font-size: 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[data-component=system-settings] {
|
||||
.root-content {
|
||||
padding: 30px;
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
|
||||
.root-col {
|
||||
@@ -11,36 +11,6 @@
|
||||
> * {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
color: constants.$main-color;
|
||||
|
||||
> .head {
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
.icon {
|
||||
font-size: constants.$SIZE_ICON_MD;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
:nth-child(n+3) {
|
||||
flex: 0 0 auto;
|
||||
color: constants.$font-main-color;
|
||||
}
|
||||
|
||||
.bt-save {
|
||||
color: constants.$main-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[data-component=system-statistics] {
|
||||
.root-content {
|
||||
padding: 30px;
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
@@ -10,81 +10,6 @@
|
||||
> .card-box {
|
||||
width: 48%;
|
||||
flex: 0 0 auto;
|
||||
|
||||
.common-card {
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
|
||||
> .head {
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: constants.$main-color;
|
||||
|
||||
.icon {
|
||||
font-size: constants.$SIZE_ICON_MD;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
:nth-child(n+3) {
|
||||
flex: 0 0 auto;
|
||||
color: constants.$font-main-color;
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
font-size: 1.1em;
|
||||
padding: 0 10px;
|
||||
gap: 10px;
|
||||
|
||||
.key, .value-percent {
|
||||
flex: 0 0 auto;
|
||||
color: constants.$font-main-color;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: constants.$font-secondary-color;
|
||||
overflow: hidden;
|
||||
|
||||
> * {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.value-chart {
|
||||
justify-content: space-around;
|
||||
width: 0;
|
||||
|
||||
> div {
|
||||
max-height: 12px;
|
||||
height: 12px;
|
||||
|
||||
> * {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.value-percent {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.big-chart {
|
||||
width: 0;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
> * {
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[data-component=system-tools-base] {
|
||||
.root-content {
|
||||
padding: 30px;
|
||||
padding: 20px;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
@@ -13,13 +13,14 @@
|
||||
}
|
||||
|
||||
>*:first-child {
|
||||
width: 0;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
> *:nth-child(2) {
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
height: calc(100vh - 60px);
|
||||
height: calc(100vh - 40px);
|
||||
}
|
||||
|
||||
.close-editor-btn {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[data-component=system-tools-code] {
|
||||
padding: 30px;
|
||||
padding: 20px;
|
||||
|
||||
.card-box {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[data-component=system-tools-execute] {
|
||||
padding: 30px;
|
||||
padding: 20px;
|
||||
|
||||
.card-box {
|
||||
width: 100%;
|
||||
|
||||
43
src/renderer/src/assets/css/pages/system/tools/template.scss
Normal file
@@ -0,0 +1,43 @@
|
||||
@use '@/assets/css/constants' as constants;
|
||||
|
||||
[data-component=system-tools-template] {
|
||||
.root-content {
|
||||
padding: 20px;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.has-edited::after {
|
||||
content: '*';
|
||||
color: constants.$font-secondary-color;
|
||||
}
|
||||
|
||||
>*:first-child {
|
||||
width: 0;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
> *:nth-child(2) {
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
height: calc(100vh - 40px);
|
||||
}
|
||||
|
||||
.close-editor-btn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background-color: constants.$font-secondary-color;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
color: white;
|
||||
opacity: 0.6;
|
||||
box-shadow: 2px 2px 10px 0 rgba(0,0,0,0.2);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,14 @@
|
||||
[data-component=tools-framework] {
|
||||
.left-panel {
|
||||
background-color: constants.$origin-color;
|
||||
|
||||
.menu-droppable {
|
||||
display: flex;
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
[data-component=tools-edit] {
|
||||
padding: 20px;
|
||||
|
||||
.card-box {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.root-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -13,10 +19,11 @@
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.draggable-content {
|
||||
position: fixed;
|
||||
inset-inline-end: 32px;
|
||||
inset-inline-end: 48px;
|
||||
inset-block-end: 48px;
|
||||
|
||||
> * {
|
||||
|
||||
@@ -3,59 +3,18 @@
|
||||
|
||||
[data-component=tools] {
|
||||
.root-content {
|
||||
padding: 30px;
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
|
||||
.own-content {
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
|
||||
> .card-box {
|
||||
> .card-box, > div {
|
||||
width: 180px;
|
||||
height: 290px;
|
||||
flex: 0 0 auto;
|
||||
|
||||
.common-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
|
||||
> * {
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.version-select {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
width: 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
49
src/renderer/src/assets/css/pages/tools/local.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
[data-component=tools-source] {
|
||||
padding: 30px;
|
||||
padding: 20px;
|
||||
|
||||
.card-box {
|
||||
width: 100%;
|
||||
|
||||
@@ -21,116 +21,15 @@
|
||||
}
|
||||
|
||||
.root-content {
|
||||
padding: 30px;
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
> .card-box {
|
||||
> div {
|
||||
width: 180px;
|
||||
height: 290px;
|
||||
flex: 0 0 auto;
|
||||
cursor: pointer;
|
||||
|
||||
.common-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
|
||||
> * {
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
padding-top: 40px;
|
||||
padding-bottom: 20px;
|
||||
color: constants.$production-color;
|
||||
font-size: constants.$SIZE_ICON_XL;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
width: constants.$SIZE_ICON_XL;
|
||||
}
|
||||
}
|
||||
|
||||
.version {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.info {
|
||||
padding-top: 20px;
|
||||
|
||||
.tool-name {
|
||||
font-weight: bolder;
|
||||
font-size: 1.6em;
|
||||
}
|
||||
|
||||
.tool-desc {
|
||||
margin-top: 10px;
|
||||
color: constants.$font-secondary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
margin-top: auto;
|
||||
flex-direction: row;
|
||||
justify-content: end;
|
||||
padding-bottom: 10px;
|
||||
gap: 10px;
|
||||
|
||||
.avatar {
|
||||
> * {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.author-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.operation {
|
||||
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 {
|
||||
@@ -141,4 +40,10 @@
|
||||
color: constants.$font-secondary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.android-qrcode {
|
||||
align-items: center;
|
||||
transform: translateX(-16px);
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,9 @@
|
||||
[data-component=tools-store-user] .root-content {
|
||||
padding: {
|
||||
top: 80px;
|
||||
left: 30px;
|
||||
right: 30px;
|
||||
bottom: 30px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
};
|
||||
|
||||
.root-box {
|
||||
@@ -68,111 +68,10 @@
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
> .card-box {
|
||||
> div {
|
||||
width: 180px;
|
||||
height: 290px;
|
||||
flex: 0 0 auto;
|
||||
cursor: pointer;
|
||||
|
||||
.common-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
|
||||
> * {
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
padding-top: 40px;
|
||||
padding-bottom: 20px;
|
||||
color: constants.$production-color;
|
||||
font-size: constants.$SIZE_ICON_XL;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
width: constants.$SIZE_ICON_XL;
|
||||
}
|
||||
}
|
||||
|
||||
.version {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.info {
|
||||
padding-top: 20px;
|
||||
|
||||
.tool-name {
|
||||
font-weight: bolder;
|
||||
font-size: 1.6em;
|
||||
}
|
||||
|
||||
.tool-desc {
|
||||
margin-top: 10px;
|
||||
color: constants.$font-secondary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
margin-top: auto;
|
||||
flex-direction: row;
|
||||
justify-content: end;
|
||||
padding-bottom: 10px;
|
||||
gap: 10px;
|
||||
|
||||
.avatar {
|
||||
> * {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.author-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.operation {
|
||||
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 {
|
||||
@@ -185,4 +84,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.android-qrcode {
|
||||
align-items: center;
|
||||
transform: translateX(-16px);
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
[data-component=tools-view] {
|
||||
padding: 30px;
|
||||
padding: 20px;
|
||||
|
||||
.card-box {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
[data-component=user] .root-content {
|
||||
padding: {
|
||||
top: 80px;
|
||||
left: 30px;
|
||||
right: 30px;
|
||||
bottom: 30px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
};
|
||||
|
||||
.card-box {
|
||||
@@ -70,6 +70,8 @@
|
||||
}
|
||||
|
||||
.url {
|
||||
cursor: pointer;
|
||||
|
||||
> span {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
1
src/renderer/src/assets/svg/arrowDown.svg
Normal 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 |
1
src/renderer/src/assets/svg/browser.svg
Normal 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 |
1
src/renderer/src/assets/svg/desktop.svg
Normal 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 |
1
src/renderer/src/assets/svg/download.svg
Normal 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 |
1
src/renderer/src/assets/svg/handle.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M7 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 2zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 14zm6-8a2 2 0 1 0-.001-4.001A2 2 0 0 0 13 6zm0 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 14z" /></svg>
|
||||
|
After Width: | Height: | Size: 325 B |
1
src/renderer/src/assets/svg/info.svg
Normal 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 |
1
src/renderer/src/assets/svg/installed.svg
Normal 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 |
1
src/renderer/src/assets/svg/mobile.svg
Normal 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 |
1
src/renderer/src/assets/svg/receive.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M927.929032 640.065509a31.994541 31.994541 0 0 0-31.994541 31.994541v127.978163c0 52.918971-43.064652 95.983623-95.983623 95.983623h-575.901736c-52.918971 0-95.983623-43.064652-95.983623-95.983623v-127.978163a31.994541 31.994541 0 0 0-63.989082 0v127.978163C64.076427 888.279157 135.872177 960.010918 224.049132 960.010918h575.901736c88.240944 0 159.972705-71.731761 159.972705-159.972705v-127.978163a31.994541 31.994541 0 0 0-31.994541-31.994541z" /><path d="M412.113043 777.386079c18.684812 18.684812 43.256619 28.027218 67.892416 28.027217s49.143615-9.342406 67.892416-28.027217l274.705128-274.705129A31.930552 31.930552 0 0 0 799.950868 448.098263h-159.140846a629.524587 629.524587 0 0 1 44.408423-202.909378L765.652721 44.0712a32.122519 32.122519 0 0 0-7.998636-35.449951 32.122519 32.122519 0 0 0-35.961864-5.055137L566.582687 81.120879A443.124392 443.124392 0 0 0 321.184558 448.098263H160.06005a31.994541 31.994541 0 0 0-22.652135 54.646676l274.705128 274.64114zM352.027295 512.023356a31.994541 31.994541 0 0 0 31.994541-31.994541A379.711212 379.711212 0 0 1 595.185806 138.391107l79.858375-39.929187-49.207604 122.923026A692.7458 692.7458 0 0 0 575.989082 480.092804a31.994541 31.994541 0 0 0 31.994541 31.994541h114.732424l-220.058453 220.058453a31.994541 31.994541 0 0 1-45.240281 0L237.294872 512.087345 352.027295 512.023356z" /></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
src/renderer/src/assets/svg/rotateLeft.svg
Normal 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 |
1
src/renderer/src/assets/svg/rotateRight.svg
Normal 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 |
1
src/renderer/src/assets/svg/star.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1151 1024"><path d="M1056.069394 342.982943L763.905526 300.38821 633.321676 35.620955c-23.397106-47.194163-91.188723-47.794089-114.785804 0L387.952021 300.38821 95.788154 342.982943c-52.39352 7.59906-73.390924 72.191072-35.395623 109.186496l211.373859 205.974527-49.993817 290.964016c-8.998887 52.593496 46.394262 91.988624 92.788525 67.391666L575.928774 879.116638l261.367676 137.38301c46.394262 24.396983 101.787412-14.79817 92.788525-67.391666l-49.993818-290.964016 211.373859-205.974527c37.995301-36.995425 16.997898-101.587436-35.395622-109.186496zM777.103894 624.548121l47.394139 276.765772L575.928774 770.730042l-248.569259 130.583851 47.394139-276.765772-201.175121-195.975763 277.965624-40.395005 124.384617-251.968838 124.384617 251.968838 277.965623 40.395005-201.17512 195.975763z" /></svg>
|
||||
|
After Width: | Height: | Size: 856 B |
1
src/renderer/src/assets/svg/starFilled.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1151 1024"><path d="M518.535872 35.620955L387.952021 300.38821 95.788154 342.982943c-52.39352 7.59906-73.390924 72.191072-35.395623 109.186496l211.373859 205.974527-49.993817 290.964016c-8.998887 52.593496 46.394262 91.988624 92.788525 67.391666L575.928774 879.116638l261.367676 137.38301c46.394262 24.396983 101.787412-14.79817 92.788525-67.391666l-49.993818-290.964016 211.373859-205.974527c37.995301-36.995425 16.997898-101.587436-35.395622-109.186496L763.905526 300.38821 633.321676 35.620955c-23.397106-47.194163-91.188723-47.794089-114.785804 0z" /></svg>
|
||||
|
After Width: | Height: | Size: 615 B |
1
src/renderer/src/assets/svg/zoom.svg
Normal 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 |
@@ -76,11 +76,11 @@ export const useEditor = () => {
|
||||
export const useTypesProgress = () => {
|
||||
const [progress, setProgress] = useState(0)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [finished, setFinished] = useState(false)
|
||||
const [isFinished, setIsFinished] = useState(false)
|
||||
|
||||
const onWatch = (typeHelper: TypeHelper) => {
|
||||
const handleStarted = () => {
|
||||
setFinished(false)
|
||||
setIsFinished(false)
|
||||
}
|
||||
typeHelper.addListener('started', handleStarted)
|
||||
|
||||
@@ -91,7 +91,7 @@ export const useTypesProgress = () => {
|
||||
typeHelper.addListener('progress', handleProgress)
|
||||
|
||||
const handleFinished = () => {
|
||||
setFinished(true)
|
||||
setIsFinished(true)
|
||||
}
|
||||
typeHelper.addListener('progress', handleFinished)
|
||||
|
||||
@@ -105,7 +105,7 @@ export const useTypesProgress = () => {
|
||||
return {
|
||||
progress,
|
||||
total,
|
||||
finished,
|
||||
finished: isFinished,
|
||||
onWatch
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { loader } from '@monaco-editor/react'
|
||||
import * as monaco from 'monaco-editor'
|
||||
|
||||
loader.config({
|
||||
paths: {
|
||||
vs: 'https://unpkg.com/monaco-editor@0.45.0/min/vs'
|
||||
}
|
||||
monaco
|
||||
})
|
||||
|
||||
@@ -31,7 +31,7 @@ const Item = ({
|
||||
}: ItemProps) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const [fileName, setFileName] = useState(value)
|
||||
const [creating, setCreating] = useState(prop.creating)
|
||||
const [isCreating, setIsCreating] = useState(prop.creating)
|
||||
|
||||
const handleOnClick = () => {
|
||||
if (hasEditing) {
|
||||
@@ -52,35 +52,35 @@ const Item = ({
|
||||
}
|
||||
|
||||
const finishNameFile = () => {
|
||||
if (!creating || onValidate ? !onValidate?.(fileName, value) : false) {
|
||||
if (!isCreating || onValidate ? !onValidate?.(fileName, value) : false) {
|
||||
inputRef.current?.focus()
|
||||
return
|
||||
}
|
||||
|
||||
if (fileName === value && active) {
|
||||
setCreating(false)
|
||||
setIsCreating(false)
|
||||
setHasEditing?.(false)
|
||||
return
|
||||
}
|
||||
|
||||
onOk?.(fileName)
|
||||
setCreating(false)
|
||||
setIsCreating(false)
|
||||
setHasEditing?.(false)
|
||||
}
|
||||
|
||||
const cancelNameFile = () => {
|
||||
setFileName(value)
|
||||
setCreating(false)
|
||||
setIsCreating(false)
|
||||
setHasEditing?.(false)
|
||||
onCancel?.()
|
||||
}
|
||||
|
||||
const handleOnDoubleClick = () => {
|
||||
if (readonly || creating || hasEditing) {
|
||||
if (readonly || isCreating || hasEditing) {
|
||||
return
|
||||
}
|
||||
|
||||
setCreating(true)
|
||||
setIsCreating(true)
|
||||
setHasEditing?.(true)
|
||||
setFileName(value)
|
||||
setTimeout(() => {
|
||||
@@ -112,7 +112,7 @@ const Item = ({
|
||||
className={`tab-item${active ? ' active' : ''}${className ? ` ${className}` : ''}`}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{creating ? (
|
||||
{isCreating ? (
|
||||
<div className={'tab-item-input'}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
|
||||
@@ -34,7 +34,7 @@ const FileSelector = ({
|
||||
}: FileSelectorProps) => {
|
||||
const hideScrollbarRef = useRef<HideScrollbarElement>(null)
|
||||
const [tabs, setTabs] = useState<string[]>([])
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const [hasEditing, setHasEditing] = useState(false)
|
||||
|
||||
const getMaxSequenceTabName = (filesName: string[]) => {
|
||||
@@ -56,7 +56,7 @@ const FileSelector = ({
|
||||
}
|
||||
|
||||
setTabs([...tabs, getMaxSequenceTabName(tabs)])
|
||||
setCreating(true)
|
||||
setIsCreating(true)
|
||||
setTimeout(() => {
|
||||
hideScrollbarRef.current?.scrollRight(1000)
|
||||
})
|
||||
@@ -64,16 +64,16 @@ const FileSelector = ({
|
||||
|
||||
const handleOnCancel = () => {
|
||||
onError?.('')
|
||||
if (!creating) {
|
||||
if (!isCreating) {
|
||||
return
|
||||
}
|
||||
tabs.pop()
|
||||
setTabs([...tabs])
|
||||
setCreating(false)
|
||||
setIsCreating(false)
|
||||
}
|
||||
|
||||
const handleOnClickTab = (fileName: string) => {
|
||||
if (creating) {
|
||||
if (isCreating) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -97,9 +97,9 @@ const FileSelector = ({
|
||||
}
|
||||
|
||||
const handleOnSaveTab = (value: string, item: string) => {
|
||||
if (creating) {
|
||||
if (isCreating) {
|
||||
onAddFile?.(value)
|
||||
setCreating(false)
|
||||
setIsCreating(false)
|
||||
} else {
|
||||
onUpdateFileName?.(value, item)
|
||||
}
|
||||
@@ -166,14 +166,20 @@ const FileSelector = ({
|
||||
<>
|
||||
<div data-component={'playground-file-selector'} className={'tab'}>
|
||||
<div className={'multiple'}>
|
||||
<HideScrollbar ref={hideScrollbarRef}>
|
||||
<HideScrollbar
|
||||
ref={hideScrollbarRef}
|
||||
autoHideWaitingTime={800}
|
||||
scrollbarWidth={1}
|
||||
scrollbarAsidePadding={0}
|
||||
scrollbarEdgePadding={0}
|
||||
>
|
||||
<FlexBox direction={'horizontal'} className={'tab-content'}>
|
||||
{tabs.map((item, index) => (
|
||||
<Item
|
||||
key={index + item}
|
||||
value={item}
|
||||
active={selectedFileName === item}
|
||||
creating={creating}
|
||||
creating={isCreating}
|
||||
readonly={readonly || notRemovableFiles.includes(item)}
|
||||
hasEditing={hasEditing}
|
||||
setHasEditing={setHasEditing}
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
import { ChangeEvent } from 'react'
|
||||
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 HideScrollbar from '@/components/common/HideScrollbar'
|
||||
|
||||
interface RenderProps {
|
||||
iframeKey: string
|
||||
compiledCode: string
|
||||
mobileMode?: boolean
|
||||
}
|
||||
|
||||
interface IMessage {
|
||||
type: 'LOADED' | 'ERROR' | 'UPDATE' | 'DONE'
|
||||
type: 'LOADED' | 'ERROR' | 'UPDATE' | 'DONE' | 'SCALE'
|
||||
msg: string
|
||||
data: {
|
||||
compiledCode: string
|
||||
compiledCode?: string
|
||||
zoom?: number
|
||||
}
|
||||
}
|
||||
|
||||
interface IDevice {
|
||||
name: string
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
const getIframeUrl = (iframeRaw: string) => {
|
||||
const shimsUrl = '//unpkg.com/es-module-shims/dist/es-module-shims.js'
|
||||
// 判断浏览器是否支持esm ,不支持esm就引入es-module-shims
|
||||
@@ -29,12 +40,115 @@ const getIframeUrl = (iframeRaw: string) => {
|
||||
|
||||
const iframeUrl = getIframeUrl(iframeRaw)
|
||||
|
||||
const Render = ({ iframeKey, compiledCode }: RenderProps) => {
|
||||
const Render = ({ iframeKey, compiledCode, mobileMode = false }: RenderProps) => {
|
||||
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(() => {
|
||||
if (loaded) {
|
||||
if (isLoaded) {
|
||||
iframeRef.current?.contentWindow?.postMessage(
|
||||
{
|
||||
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
|
||||
data-component={'playground-output-preview-render'}
|
||||
key={iframeKey}
|
||||
ref={iframeRef}
|
||||
src={iframeUrl}
|
||||
onLoad={() => setLoaded(true)}
|
||||
onLoad={() => setIsLoaded(true)}
|
||||
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'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
window.addEventListener("message", ({ data }) => {
|
||||
if (data?.type === "UPDATE") {
|
||||
// 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
|
||||
const appSrcElement = document.querySelector("#appSrc");
|
||||
@@ -36,6 +36,10 @@
|
||||
document.body.appendChild(script);
|
||||
URL.revokeObjectURL(oldSrc);
|
||||
}
|
||||
|
||||
if (data?.type === "SCALE") {
|
||||
document.getElementById("root").style.zoom = data.data.zoom
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script type="module" id="appSrc"></script>
|
||||
|
||||
@@ -10,6 +10,7 @@ interface PreviewProps {
|
||||
entryPoint: string
|
||||
preExpansionCode?: string
|
||||
postExpansionCode?: string
|
||||
mobileMode?: boolean
|
||||
}
|
||||
|
||||
const Preview = ({
|
||||
@@ -18,7 +19,8 @@ const Preview = ({
|
||||
importMap,
|
||||
entryPoint,
|
||||
preExpansionCode = '',
|
||||
postExpansionCode = ''
|
||||
postExpansionCode = '',
|
||||
mobileMode = false
|
||||
}: PreviewProps) => {
|
||||
const [errorMsg, setErrorMsg] = useState('')
|
||||
const [compiledCode, setCompiledCode] = useState('')
|
||||
@@ -41,7 +43,7 @@ const Preview = ({
|
||||
|
||||
return (
|
||||
<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>}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
[data-component=playground-preview] {
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: 0;
|
||||
|
||||
.playground-error-message {
|
||||
position: absolute;
|
||||
|
||||
@@ -1,6 +1,80 @@
|
||||
@use '@/assets/css/constants' as constants;
|
||||
|
||||
[data-component=playground-output-preview-render] {
|
||||
border: none;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ interface OutputProps {
|
||||
entryPoint: string
|
||||
preExpansionCode?: string
|
||||
postExpansionCode?: string
|
||||
mobileMode?: boolean
|
||||
}
|
||||
|
||||
const Output = ({
|
||||
@@ -19,7 +20,8 @@ const Output = ({
|
||||
importMap,
|
||||
entryPoint,
|
||||
preExpansionCode,
|
||||
postExpansionCode
|
||||
postExpansionCode,
|
||||
mobileMode = false
|
||||
}: OutputProps) => {
|
||||
const [selectedTab, setSelectedTab] = useState('Preview')
|
||||
|
||||
@@ -42,6 +44,7 @@ const Output = ({
|
||||
entryPoint={entryPoint}
|
||||
preExpansionCode={preExpansionCode}
|
||||
postExpansionCode={postExpansionCode}
|
||||
mobileMode={mobileMode}
|
||||
/>
|
||||
)}
|
||||
{selectedTab === 'Transform' && <Transform file={files[selectedFileName]} />}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import esbuild, { Loader, OnLoadArgs, Plugin, PluginBuild } from 'esbuild-wasm'
|
||||
import localforage from 'localforage'
|
||||
import axios from 'axios'
|
||||
import esbuildWasmUrl from 'esbuild-wasm/esbuild.wasm?url'
|
||||
import { IFiles, IImportMap } from '@/components/Playground/shared'
|
||||
import { cssToJs, jsonToJs, addReactImport } from '@/components/Playground/files'
|
||||
|
||||
@@ -16,7 +17,7 @@ class Compiler {
|
||||
void esbuild
|
||||
.initialize({
|
||||
worker: true,
|
||||
wasmURL: 'https://esm.sh/esbuild-wasm@0.20.1/esbuild.wasm'
|
||||
wasmURL: esbuildWasmUrl
|
||||
})
|
||||
.finally(() => {
|
||||
this.init = true
|
||||
|
||||
@@ -21,6 +21,8 @@ interface HideScrollbarProps
|
||||
minHeight?: string | number
|
||||
scrollbarWidth?: string | number
|
||||
autoHideWaitingTime?: number
|
||||
scrollbarAsidePadding?: number
|
||||
scrollbarEdgePadding?: number
|
||||
}
|
||||
|
||||
export interface HideScrollbarElement {
|
||||
@@ -74,6 +76,8 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
|
||||
children,
|
||||
style,
|
||||
className,
|
||||
scrollbarAsidePadding = 12,
|
||||
scrollbarEdgePadding = 4,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
@@ -179,16 +183,16 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
|
||||
const [verticalScrollbarWidth, setVerticalScrollbarWidth] = useState(0)
|
||||
const [verticalScrollbarLength, setVerticalScrollbarLength] = useState(100)
|
||||
const [verticalScrollbarPosition, setVerticalScrollbarPosition] = useState(0)
|
||||
const [verticalScrollbarOnClick, setVerticalScrollbarOnClick] = useState(false)
|
||||
const [verticalScrollbarOnTouch, setVerticalScrollbarOnTouch] = useState(false)
|
||||
const [verticalScrollbarAutoHide, setVerticalScrollbarAutoHide] = useState(false)
|
||||
const [isVerticalScrollbarOnClick, setIsVerticalScrollbarOnClick] = useState(false)
|
||||
const [isVerticalScrollbarOnTouch, setIsVerticalScrollbarOnTouch] = useState(false)
|
||||
const [isVerticalScrollbarAutoHide, setIsVerticalScrollbarAutoHide] = useState(false)
|
||||
|
||||
const [horizontalScrollbarWidth, setHorizontalScrollbarWidth] = useState(0)
|
||||
const [horizontalScrollbarLength, setHorizontalScrollbarLength] = useState(100)
|
||||
const [horizontalScrollbarPosition, setHorizontalScrollbarPosition] = useState(0)
|
||||
const [horizontalScrollbarOnClick, setHorizontalScrollbarOnClick] = useState(false)
|
||||
const [horizontalScrollbarOnTouch, setHorizontalScrollbarOnTouch] = useState(false)
|
||||
const [horizontalScrollbarAutoHide, setHorizontalScrollbarAutoHide] = useState(false)
|
||||
const [isHorizontalScrollbarOnClick, setIsHorizontalScrollbarOnClick] = useState(false)
|
||||
const [isHorizontalScrollbarOnTouch, setIsHorizontalScrollbarOnTouch] = useState(false)
|
||||
const [isHorizontalScrollbarAutoHide, setIsHorizontalScrollbarAutoHide] = useState(false)
|
||||
|
||||
const isPreventAnyScroll =
|
||||
isPreventScroll || isPreventVerticalScroll || isPreventHorizontalScroll
|
||||
@@ -197,10 +201,10 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
|
||||
if (autoHideWaitingTime === undefined) {
|
||||
return
|
||||
}
|
||||
setVerticalScrollbarAutoHide(false)
|
||||
setIsVerticalScrollbarAutoHide(false)
|
||||
if (autoHideWaitingTime > 0) {
|
||||
setTimeout(() => {
|
||||
setVerticalScrollbarAutoHide(true)
|
||||
setIsVerticalScrollbarAutoHide(true)
|
||||
}, autoHideWaitingTime)
|
||||
}
|
||||
}, [autoHideWaitingTime, verticalScrollbarPosition])
|
||||
@@ -209,10 +213,10 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
|
||||
if (autoHideWaitingTime === undefined) {
|
||||
return
|
||||
}
|
||||
setHorizontalScrollbarAutoHide(false)
|
||||
setIsHorizontalScrollbarAutoHide(false)
|
||||
if (autoHideWaitingTime > 0) {
|
||||
setTimeout(() => {
|
||||
setHorizontalScrollbarAutoHide(true)
|
||||
setIsHorizontalScrollbarAutoHide(true)
|
||||
}, autoHideWaitingTime)
|
||||
}
|
||||
}, [autoHideWaitingTime, horizontalScrollbarPosition])
|
||||
@@ -314,20 +318,20 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
|
||||
}
|
||||
switch (scrollbarFlag) {
|
||||
case 'vertical':
|
||||
setVerticalScrollbarOnClick(true)
|
||||
setIsVerticalScrollbarOnClick(true)
|
||||
break
|
||||
case 'horizontal':
|
||||
setHorizontalScrollbarOnClick(true)
|
||||
setIsHorizontalScrollbarOnClick(true)
|
||||
break
|
||||
}
|
||||
break
|
||||
case 'up':
|
||||
case 'leave':
|
||||
setVerticalScrollbarOnClick(false)
|
||||
setHorizontalScrollbarOnClick(false)
|
||||
setIsVerticalScrollbarOnClick(false)
|
||||
setIsHorizontalScrollbarOnClick(false)
|
||||
break
|
||||
case 'move':
|
||||
if (verticalScrollbarOnClick) {
|
||||
if (isVerticalScrollbarOnClick) {
|
||||
rootRef.current?.scrollTo({
|
||||
top:
|
||||
rootRef.current?.scrollTop +
|
||||
@@ -337,7 +341,7 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
|
||||
behavior: 'instant'
|
||||
})
|
||||
}
|
||||
if (horizontalScrollbarOnClick) {
|
||||
if (isHorizontalScrollbarOnClick) {
|
||||
rootRef.current?.scrollTo({
|
||||
left:
|
||||
rootRef.current?.scrollLeft +
|
||||
@@ -368,23 +372,23 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
|
||||
}
|
||||
switch (scrollbarFlag) {
|
||||
case 'vertical':
|
||||
setVerticalScrollbarOnTouch(true)
|
||||
setIsVerticalScrollbarOnTouch(true)
|
||||
break
|
||||
case 'horizontal':
|
||||
setHorizontalScrollbarOnTouch(true)
|
||||
setIsHorizontalScrollbarOnTouch(true)
|
||||
break
|
||||
}
|
||||
break
|
||||
case 'end':
|
||||
case 'cancel':
|
||||
setVerticalScrollbarOnTouch(false)
|
||||
setHorizontalScrollbarOnTouch(false)
|
||||
setIsVerticalScrollbarOnTouch(false)
|
||||
setIsHorizontalScrollbarOnTouch(false)
|
||||
break
|
||||
case 'move':
|
||||
if (event.touches.length !== 1) {
|
||||
return
|
||||
}
|
||||
if (verticalScrollbarOnTouch) {
|
||||
if (isVerticalScrollbarOnTouch) {
|
||||
rootRef.current?.scrollTo({
|
||||
top:
|
||||
rootRef.current?.scrollTop +
|
||||
@@ -395,7 +399,7 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
|
||||
behavior: 'instant'
|
||||
})
|
||||
}
|
||||
if (horizontalScrollbarOnTouch) {
|
||||
if (isHorizontalScrollbarOnTouch) {
|
||||
rootRef.current?.scrollTo({
|
||||
left:
|
||||
rootRef.current?.scrollLeft +
|
||||
@@ -567,7 +571,7 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
|
||||
(!isHiddenVerticalScrollbarWhenFull || verticalScrollbarLength < 100) && (
|
||||
<div
|
||||
className={`scrollbar vertical-scrollbar${
|
||||
verticalScrollbarAutoHide ? ' hide' : ''
|
||||
isVerticalScrollbarAutoHide ? ' hide' : ''
|
||||
}`}
|
||||
style={{
|
||||
height: maskRef.current
|
||||
@@ -578,7 +582,8 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
|
||||
? maskRef.current?.clientLeft +
|
||||
maskRef.current?.clientWidth -
|
||||
1
|
||||
: undefined
|
||||
: undefined,
|
||||
padding: `${scrollbarAsidePadding}px ${scrollbarEdgePadding}px`
|
||||
}}
|
||||
>
|
||||
<div className={'box'} style={{ width: scrollbarWidth }}>
|
||||
@@ -609,7 +614,7 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
|
||||
horizontalScrollbarLength < 100) && (
|
||||
<div
|
||||
className={`scrollbar horizontal-scrollbar${
|
||||
horizontalScrollbarAutoHide ? ' hide' : ''
|
||||
isHorizontalScrollbarAutoHide ? ' hide' : ''
|
||||
}`}
|
||||
style={{
|
||||
width: maskRef.current
|
||||
@@ -620,7 +625,8 @@ const HideScrollbar = forwardRef<HideScrollbarElement, HideScrollbarProps>(
|
||||
? maskRef.current?.clientTop +
|
||||
maskRef.current?.clientHeight -
|
||||
1
|
||||
: undefined
|
||||
: undefined,
|
||||
padding: `${scrollbarAsidePadding}px ${scrollbarEdgePadding}px`
|
||||
}}
|
||||
>
|
||||
<div className={'box'} style={{ height: scrollbarWidth }}>
|
||||
|
||||
@@ -7,6 +7,7 @@ interface LoadingMaskProps extends PropsWithChildren {
|
||||
hidden?: boolean
|
||||
maskContent?: ReactNode
|
||||
}
|
||||
|
||||
const LoadingMask = (props: LoadingMaskProps) => {
|
||||
const loadingIcon = (
|
||||
<>
|
||||
|
||||
@@ -2,6 +2,7 @@ import Icon from '@ant-design/icons'
|
||||
import { COLOR_ERROR } from '@/constants/common.constants'
|
||||
import { getRedirectUrl } from '@/util/route'
|
||||
import { getAvatar, getLoginStatus, getNickname, removeToken } from '@/util/auth'
|
||||
import { navigateToLogin, navigateToUser } from '@/util/navigation'
|
||||
import { r_auth_logout } from '@/services/auth'
|
||||
|
||||
const Footer = () => {
|
||||
@@ -9,24 +10,24 @@ const Footer = () => {
|
||||
const lastMatch = matches.reduce((_, second) => second)
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const [exiting, setExiting] = useState(false)
|
||||
const [isExiting, setIsExiting] = useState(false)
|
||||
const [nickname, setNickname] = useState('')
|
||||
const [avatar, setAvatar] = useState('')
|
||||
|
||||
const handleClickAvatar = () => {
|
||||
if (getLoginStatus()) {
|
||||
navigate('/user')
|
||||
navigateToUser(navigate)
|
||||
} else {
|
||||
navigate(getRedirectUrl('/login', `${lastMatch.pathname}${location.search}`))
|
||||
navigateToLogin(navigate, undefined, `${lastMatch.pathname}${location.search}`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
if (exiting) {
|
||||
if (isExiting) {
|
||||
return
|
||||
}
|
||||
|
||||
setExiting(true)
|
||||
setIsExiting(true)
|
||||
void r_auth_logout().finally(() => {
|
||||
removeToken()
|
||||
notification.info({
|
||||
@@ -82,8 +83,8 @@ const Footer = () => {
|
||||
<div className={'content'}>
|
||||
<span hidden={!getLoginStatus()} className={'icon-exit'} onClick={handleLogout}>
|
||||
<Icon
|
||||
component={exiting ? IconOxygenLoading : IconOxygenExit}
|
||||
spin={exiting}
|
||||
component={isExiting ? IconOxygenLoading : IconOxygenExit}
|
||||
spin={isExiting}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -3,16 +3,17 @@ import Icon from '@ant-design/icons'
|
||||
import Submenu from '@/components/common/Sidebar/Submenu'
|
||||
|
||||
type ItemProps = {
|
||||
icon?: IconComponent
|
||||
icon?: IconComponent | string
|
||||
text?: string
|
||||
path: string
|
||||
children?: ReactNode
|
||||
extend?: ReactNode
|
||||
end?: boolean
|
||||
}
|
||||
|
||||
const Item = (props: ItemProps) => {
|
||||
const [submenuTop, setSubmenuTop] = useState(0)
|
||||
const [submenuLeft, setSubmenuLeft] = useState(0)
|
||||
const [submenuTop, setSubmenuTop] = useState(Number.MAX_VALUE)
|
||||
const [submenuLeft, setSubmenuLeft] = useState(Number.MAX_VALUE)
|
||||
|
||||
const showSubmenu = (e: MouseEvent) => {
|
||||
const parentElement = e.currentTarget.parentElement
|
||||
@@ -41,10 +42,21 @@ const Item = (props: ItemProps) => {
|
||||
isPending ? 'pending' : isActive ? 'active' : ''
|
||||
}
|
||||
>
|
||||
{props.icon && (
|
||||
<div className={'icon-box'}>
|
||||
{props.icon && <Icon className={'icon'} component={props.icon} />}
|
||||
{typeof props.icon === 'string' ? (
|
||||
<img
|
||||
className={'icon'}
|
||||
src={`data:image/svg+xml;base64,${props.icon}`}
|
||||
alt={'icon'}
|
||||
/>
|
||||
) : (
|
||||
<Icon className={'icon'} component={props.icon} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<span className={'text'}>{props.text}</span>
|
||||
<div className={'extend'}>{props.extend}</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
{props.children && (
|
||||
|
||||
@@ -17,18 +17,18 @@ interface SidebarProps extends PropsWithChildren {
|
||||
}
|
||||
|
||||
const Sidebar = (props: SidebarProps) => {
|
||||
const [hideSidebar, setHideSidebar] = useState(getLocalStorage('HIDE_SIDEBAR') === 'true')
|
||||
const [isHideSidebar, setIsHideSidebar] = useState(getLocalStorage('HIDE_SIDEBAR') === 'true')
|
||||
|
||||
const switchSidebar = () => {
|
||||
setLocalStorage('HIDE_SIDEBAR', !hideSidebar ? 'true' : 'false')
|
||||
setHideSidebar(!hideSidebar)
|
||||
props.onSidebarSwitch?.(hideSidebar)
|
||||
setLocalStorage('HIDE_SIDEBAR', !isHideSidebar ? 'true' : 'false')
|
||||
setIsHideSidebar(!isHideSidebar)
|
||||
props.onSidebarSwitch?.(isHideSidebar)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`sidebar${hideSidebar ? ' hide' : ''}`}
|
||||
className={`sidebar${isHideSidebar ? ' hide' : ''}`}
|
||||
style={{ width: props.width ?? 'clamp(180px, 20vw, 240px)' }}
|
||||
>
|
||||
<div className={'title'}>
|
||||
|
||||
57
src/renderer/src/components/common/UrlCard.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react'
|
||||
import Icon from '@ant-design/icons'
|
||||
import VanillaTilt, { TiltOptions } from 'vanilla-tilt'
|
||||
import '@/assets/css/components/common/url-card.scss'
|
||||
import Card from '@/components/common/Card'
|
||||
import FlexBox from '@/components/common/FlexBox'
|
||||
|
||||
interface UrlCardProps extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
|
||||
icon: IconComponent
|
||||
description?: ReactNode
|
||||
options?: TiltOptions
|
||||
url?: string
|
||||
}
|
||||
|
||||
const UrlCard = ({
|
||||
style,
|
||||
icon,
|
||||
description,
|
||||
options = {
|
||||
reverse: true,
|
||||
max: 8,
|
||||
glare: true,
|
||||
scale: 1.03
|
||||
},
|
||||
url,
|
||||
children,
|
||||
...props
|
||||
}: UrlCardProps) => {
|
||||
const navigate = useNavigate()
|
||||
const cardRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
cardRef.current && VanillaTilt.init(cardRef.current, options)
|
||||
}, [options])
|
||||
|
||||
const handleCardOnClick = () => {
|
||||
url && navigate(url)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
data-component={'component-url-card'}
|
||||
style={{ overflow: 'visible', ...style }}
|
||||
{...props}
|
||||
ref={cardRef}
|
||||
onClick={handleCardOnClick}
|
||||
>
|
||||
<FlexBox className={'url-card'}>
|
||||
<Icon component={icon} className={'icon'} />
|
||||
<div className={'text'}>{children}</div>
|
||||
<div className={'description'}>{description}</div>
|
||||
</FlexBox>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default UrlCard
|
||||
27
src/renderer/src/components/dnd/DragHandle.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { HandleContextInst } from '@/components/dnd/HandleContext'
|
||||
import Icon from '@ant-design/icons'
|
||||
import '@/assets/css/components/dnd/drag-handle.scss'
|
||||
|
||||
interface DragHandleProps {
|
||||
padding?: string | number
|
||||
}
|
||||
|
||||
const DragHandle = ({ padding }: DragHandleProps) => {
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
const { attributes, listeners, ref } = useContext(HandleContextInst)
|
||||
|
||||
return (
|
||||
<button
|
||||
data-component={'component-drag-handle'}
|
||||
style={{ padding }}
|
||||
ref={ref}
|
||||
className={'drag-handle'}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
<Icon component={IconOxygenHandle} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default DragHandle
|
||||
47
src/renderer/src/components/dnd/Draggable.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { CSSProperties, PropsWithChildren } from 'react'
|
||||
import { useDraggable } from '@dnd-kit/core'
|
||||
import { HandleContext, HandleContextInst } from '@/components/dnd/HandleContext'
|
||||
|
||||
interface DraggableProps extends PropsWithChildren {
|
||||
id: string
|
||||
data: ToolMenuItem
|
||||
}
|
||||
|
||||
const Draggable = ({ id, data, children }: DraggableProps) => {
|
||||
const {
|
||||
attributes,
|
||||
isDragging,
|
||||
listeners,
|
||||
setNodeRef: draggableRef,
|
||||
setActivatorNodeRef,
|
||||
transform
|
||||
} = useDraggable({
|
||||
id,
|
||||
data
|
||||
})
|
||||
const context = useMemo<HandleContext>(
|
||||
() => ({
|
||||
attributes,
|
||||
listeners,
|
||||
ref: setActivatorNodeRef
|
||||
}),
|
||||
[attributes, listeners, setActivatorNodeRef]
|
||||
)
|
||||
const style: CSSProperties | undefined = transform
|
||||
? {
|
||||
opacity: isDragging ? 0 : undefined,
|
||||
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||
zIndex: 10000
|
||||
}
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<HandleContextInst.Provider value={context}>
|
||||
<div ref={draggableRef} style={style}>
|
||||
{children}
|
||||
</div>
|
||||
</HandleContextInst.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default Draggable
|
||||
22
src/renderer/src/components/dnd/DraggableOverlay.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { defaultDropAnimationSideEffects, DragOverlay, DropAnimation } from '@dnd-kit/core'
|
||||
|
||||
interface DraggableOverlayProps extends PropsWithChildren {
|
||||
isDelete?: boolean
|
||||
}
|
||||
|
||||
const dropAnimationConfig: DropAnimation = {
|
||||
sideEffects: defaultDropAnimationSideEffects({
|
||||
styles: {
|
||||
active: {
|
||||
opacity: '0.4'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const DraggableOverlay = ({ children }: DraggableOverlayProps) => {
|
||||
return <DragOverlay dropAnimation={dropAnimationConfig}>{children}</DragOverlay>
|
||||
}
|
||||
|
||||
export default DraggableOverlay
|
||||
14
src/renderer/src/components/dnd/DropMask.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import '@/assets/css/components/dnd/drop-mask.scss'
|
||||
import Icon from '@ant-design/icons'
|
||||
|
||||
const DropMask = () => {
|
||||
return (
|
||||
<div data-component={'component-drop-mask'}>
|
||||
<div className={'drop-mask-border'}>
|
||||
<Icon component={IconOxygenReceive} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DropMask
|
||||
16
src/renderer/src/components/dnd/Droppable.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { DetailedHTMLProps, HTMLAttributes } from 'react'
|
||||
import { useDroppable } from '@dnd-kit/core'
|
||||
|
||||
interface DroppableProps extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
|
||||
id: string
|
||||
}
|
||||
|
||||
const Droppable = ({ id, ...props }: DroppableProps) => {
|
||||
const { setNodeRef: droppableRef } = useDroppable({
|
||||
id
|
||||
})
|
||||
|
||||
return <div {...props} ref={droppableRef} />
|
||||
}
|
||||
|
||||
export default Droppable
|
||||
13
src/renderer/src/components/dnd/HandleContext.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { DraggableSyntheticListeners } from '@dnd-kit/core'
|
||||
|
||||
export interface HandleContext {
|
||||
attributes: Record<string, any>
|
||||
listeners: DraggableSyntheticListeners
|
||||
ref(node: HTMLElement | null): void
|
||||
}
|
||||
|
||||
export const HandleContextInst = createContext<HandleContext>({
|
||||
attributes: {},
|
||||
listeners: undefined,
|
||||
ref() {}
|
||||
})
|
||||
54
src/renderer/src/components/dnd/Sortable.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { CSSProperties, PropsWithChildren } from 'react'
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { HandleContext, HandleContextInst } from '@/components/dnd/HandleContext'
|
||||
|
||||
interface SortableProps extends PropsWithChildren {
|
||||
id: string
|
||||
data: ToolMenuItem
|
||||
isDelete?: boolean
|
||||
}
|
||||
|
||||
const Sortable = ({ id, data, isDelete, children }: SortableProps) => {
|
||||
const {
|
||||
attributes,
|
||||
isDragging,
|
||||
listeners,
|
||||
setNodeRef: draggableRef,
|
||||
setActivatorNodeRef,
|
||||
transform,
|
||||
transition
|
||||
} = useSortable({
|
||||
id,
|
||||
data
|
||||
})
|
||||
const context = useMemo<HandleContext>(
|
||||
() => ({
|
||||
attributes,
|
||||
listeners,
|
||||
ref: setActivatorNodeRef
|
||||
}),
|
||||
[attributes, listeners, setActivatorNodeRef]
|
||||
)
|
||||
const style: CSSProperties | undefined = transform
|
||||
? {
|
||||
opacity: isDragging ? 0.4 : undefined,
|
||||
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||
zIndex: 10000,
|
||||
transition
|
||||
}
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<HandleContextInst.Provider value={context}>
|
||||
<div
|
||||
ref={draggableRef}
|
||||
style={style}
|
||||
className={isDragging && isDelete ? 'delete' : undefined}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</HandleContextInst.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default Sortable
|
||||
48
src/renderer/src/components/system/SettingCard.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { PropsWithChildren, ReactNode } from 'react'
|
||||
import Icon from '@ant-design/icons'
|
||||
import '@/assets/css/components/system/setting-card.scss'
|
||||
import Card from '@/components/common/Card'
|
||||
import FlexBox from '@/components/common/FlexBox'
|
||||
import Permission from '@/components/common/Permission'
|
||||
import LoadingMask from '@/components/common/LoadingMask'
|
||||
|
||||
interface SettingsCardProps extends PropsWithChildren {
|
||||
icon: IconComponent
|
||||
title: string
|
||||
loading?: boolean
|
||||
modifyOperationCode?: string[]
|
||||
expand?: ReactNode
|
||||
onReset?: () => void
|
||||
onSave?: () => void
|
||||
}
|
||||
export const SettingsCard = (props: SettingsCardProps) => {
|
||||
return (
|
||||
<Card data-component={'component-setting-card'}>
|
||||
<FlexBox className={'settings-card'}>
|
||||
<FlexBox direction={'horizontal'} className={'head'}>
|
||||
<Icon component={props.icon} className={'icon'} />
|
||||
<div className={'title'}>{props.title}</div>
|
||||
{!props.loading && (
|
||||
<Permission operationCode={props.modifyOperationCode}>
|
||||
{props.expand}
|
||||
<AntdButton onClick={props.onReset} title={'重置'}>
|
||||
<Icon component={IconOxygenBack} />
|
||||
</AntdButton>
|
||||
<AntdButton className={'bt-save'} onClick={props.onSave} title={'保存'}>
|
||||
<Icon component={IconOxygenSave} />
|
||||
</AntdButton>
|
||||
</Permission>
|
||||
)}
|
||||
</FlexBox>
|
||||
<LoadingMask
|
||||
maskContent={<AntdSkeleton active paragraph={{ rows: 6 }} />}
|
||||
hidden={!props.loading}
|
||||
>
|
||||
{props.children}
|
||||
</LoadingMask>
|
||||
</FlexBox>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingsCard
|
||||
35
src/renderer/src/components/system/StatisticsCard.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { PropsWithChildren, ReactNode } from 'react'
|
||||
import Icon from '@ant-design/icons'
|
||||
import '@/assets/css/components/system/statistics-card.scss'
|
||||
import Card from '@/components/common/Card'
|
||||
import FlexBox from '@/components/common/FlexBox'
|
||||
import LoadingMask from '@/components/common/LoadingMask'
|
||||
|
||||
interface StatisticsCardProps extends PropsWithChildren {
|
||||
icon: IconComponent
|
||||
title: ReactNode
|
||||
loading?: boolean
|
||||
expand?: ReactNode
|
||||
}
|
||||
|
||||
export const StatisticsCard = (props: StatisticsCardProps) => {
|
||||
return (
|
||||
<Card data-component={'component-statistics-card'} style={{ overflow: 'visible' }}>
|
||||
<FlexBox className={'statistics-card'}>
|
||||
<FlexBox direction={'horizontal'} className={'head'}>
|
||||
<Icon component={props.icon} className={'icon'} />
|
||||
<div className={'title'}>{props.title}</div>
|
||||
{props.expand}
|
||||
</FlexBox>
|
||||
<LoadingMask
|
||||
hidden={!props.loading}
|
||||
maskContent={<AntdSkeleton active paragraph={{ rows: 6 }} />}
|
||||
>
|
||||
{props.children}
|
||||
</LoadingMask>
|
||||
</FlexBox>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatisticsCard
|
||||
42
src/renderer/src/components/tools/LoadMoreCard.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import VanillaTilt from 'vanilla-tilt'
|
||||
import Icon from '@ant-design/icons'
|
||||
import '@/assets/css/components/tools/load-more-card.scss'
|
||||
import FlexBox from '@/components/common/FlexBox'
|
||||
import Card from '@/components/common/Card'
|
||||
|
||||
interface LoadMoreCardProps {
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const LoadMoreCard = ({ onClick }: LoadMoreCardProps) => {
|
||||
const cardRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
cardRef.current &&
|
||||
VanillaTilt.init(cardRef.current, {
|
||||
reverse: true,
|
||||
max: 8,
|
||||
glare: true,
|
||||
['max-glare']: 0.3,
|
||||
scale: 1.03
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Card
|
||||
data-component={'component-load-more-card'}
|
||||
style={{ overflow: 'visible' }}
|
||||
ref={cardRef}
|
||||
onClick={onClick}
|
||||
>
|
||||
<FlexBox className={'load-more-card'}>
|
||||
<div className={'icon'}>
|
||||
<Icon component={IconOxygenMore} />{' '}
|
||||
</div>
|
||||
<div className={'text'}>加载更多</div>
|
||||
</FlexBox>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoadMoreCard
|
||||
198
src/renderer/src/components/tools/LocalCard.tsx
Normal 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
|
||||
115
src/renderer/src/components/tools/RepositoryCard.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { DetailedHTMLProps, HTMLAttributes } from 'react'
|
||||
import VanillaTilt, { TiltOptions } from 'vanilla-tilt'
|
||||
import '@/assets/css/components/tools/repository-card.scss'
|
||||
import Card from '@/components/common/Card'
|
||||
import FlexBox from '@/components/common/FlexBox'
|
||||
import Draggable from '@/components/dnd/Draggable'
|
||||
import DragHandle from '@/components/dnd/DragHandle'
|
||||
|
||||
interface RepositoryCardProps
|
||||
extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
|
||||
icon: string
|
||||
toolName: string
|
||||
toolId: string
|
||||
ver: string
|
||||
platform: Platform
|
||||
options?: TiltOptions
|
||||
onOpen?: () => void
|
||||
onEdit?: () => void
|
||||
onSource?: () => void
|
||||
onPublish?: () => void
|
||||
onCancelReview?: () => void
|
||||
onDelete?: () => void
|
||||
}
|
||||
|
||||
const RepositoryCard = ({
|
||||
style,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
ref,
|
||||
icon,
|
||||
toolName,
|
||||
toolId,
|
||||
ver,
|
||||
platform,
|
||||
options = {
|
||||
reverse: true,
|
||||
max: 8,
|
||||
glare: true,
|
||||
['max-glare']: 0.3,
|
||||
scale: 1.03
|
||||
},
|
||||
onOpen,
|
||||
onEdit,
|
||||
onSource,
|
||||
onPublish,
|
||||
onCancelReview,
|
||||
onDelete,
|
||||
children,
|
||||
...props
|
||||
}: RepositoryCardProps) => {
|
||||
const cardRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
cardRef.current && VanillaTilt.init(cardRef.current, options)
|
||||
}, [options])
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
id={`!:${toolId}:${ver}:${platform}`}
|
||||
data={{ icon, toolName, toolId, authorUsername: '!', ver, platform }}
|
||||
>
|
||||
<Card
|
||||
data-component={'component-repository-card'}
|
||||
style={{ overflow: 'visible', ...style }}
|
||||
ref={cardRef}
|
||||
{...props}
|
||||
>
|
||||
<FlexBox className={'repository-card'}>
|
||||
<div className={'header'}>
|
||||
{children}
|
||||
<DragHandle />
|
||||
</div>
|
||||
<div className={'icon'}>
|
||||
<img src={`data:image/svg+xml;base64,${icon}`} alt={'Icon'} />
|
||||
</div>
|
||||
<div className={'info'}>
|
||||
<div className={'tool-name'}>{toolName}</div>
|
||||
<div className={'tool-id'}>{`ID: ${toolId}`}</div>
|
||||
</div>
|
||||
<div className={'operation'}>
|
||||
{onOpen && (
|
||||
<AntdButton onClick={onOpen} size={'small'} type={'primary'}>
|
||||
打开
|
||||
</AntdButton>
|
||||
)}
|
||||
{onEdit && onPublish && (
|
||||
<div className={'edit'}>
|
||||
<AntdButton.Group size={'small'}>
|
||||
<AntdButton onClick={onEdit}>编辑</AntdButton>
|
||||
<AntdButton onClick={onPublish}>发布</AntdButton>
|
||||
</AntdButton.Group>
|
||||
</div>
|
||||
)}
|
||||
{onSource && (
|
||||
<AntdButton size={'small'} onClick={onSource}>
|
||||
源码
|
||||
</AntdButton>
|
||||
)}
|
||||
{onCancelReview && (
|
||||
<AntdButton size={'small'} onClick={onCancelReview}>
|
||||
取消审核
|
||||
</AntdButton>
|
||||
)}
|
||||
{onDelete && (
|
||||
<AntdButton size={'small'} danger onClick={onDelete}>
|
||||
删除
|
||||
</AntdButton>
|
||||
)}
|
||||
</div>
|
||||
</FlexBox>
|
||||
</Card>
|
||||
</Draggable>
|
||||
)
|
||||
}
|
||||
|
||||
export default RepositoryCard
|
||||
407
src/renderer/src/components/tools/StoreCard.tsx
Normal 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
|
||||
@@ -1,6 +1,7 @@
|
||||
export const PRODUCTION_NAME = 'Oxygen Toolbox'
|
||||
export const STORAGE_TOKEN_KEY = 'JWT_TOKEN'
|
||||
export const STORAGE_USER_INFO_KEY = 'USER_INFO'
|
||||
export const STORAGE_TOOL_MENU_ITEM_KEY = 'TOOL_MENU_ITEM'
|
||||
export const COLOR_ORIGIN = 'white'
|
||||
export const COLOR_PRODUCTION = '#4E47BB'
|
||||
export const COLOR_MAIN = COLOR_PRODUCTION
|
||||
|
||||
@@ -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_CATEGORY = `${URL_SYS_TOOL}/category`
|
||||
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_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_CATEGORY = `${URL_TOOL}/category`
|
||||
export const URL_TOOL_DETAIL = `${URL_TOOL}/detail`
|
||||
export const URL_TOOL_FAVORITE = `${URL_TOOL_STORE}/favorite`
|
||||
|
||||
export const URL_API_V1 = '/api/v1'
|
||||
export const URL_API_V1_AVATAR_RANDOM_BASE64 = `${URL_API_V1}/avatar/base64`
|
||||
|
||||
16
src/renderer/src/electron.d.ts
vendored
Normal 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>>>
|
||||
}
|
||||
}
|
||||
36
src/renderer/src/global.d.ts
vendored
@@ -1,7 +1,14 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="./electron" />
|
||||
/// <reference types="./ant-design" />
|
||||
|
||||
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
|
||||
@@ -11,6 +18,12 @@ interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
|
||||
interface Window {
|
||||
electronAPI: _ElectronAPI
|
||||
Notification: typeof _Notification
|
||||
api: API
|
||||
}
|
||||
|
||||
interface RouteJsonObject {
|
||||
path: string
|
||||
absolutePath: string
|
||||
@@ -545,6 +558,7 @@ interface ToolBaseVo {
|
||||
name: string
|
||||
source: ToolDataVo
|
||||
dist: ToolDataVo
|
||||
platform: Platform
|
||||
compiled: boolean
|
||||
createTime: string
|
||||
updateTime: string
|
||||
@@ -555,6 +569,7 @@ interface ToolBaseAddEditParam {
|
||||
name?: string
|
||||
source?: string
|
||||
dist?: string
|
||||
platform?: Platform
|
||||
}
|
||||
|
||||
interface ToolTemplateVo {
|
||||
@@ -562,6 +577,7 @@ interface ToolTemplateVo {
|
||||
name: string
|
||||
baseId: string
|
||||
source: ToolDataVo
|
||||
platform: Platform
|
||||
entryPoint: string
|
||||
enable: boolean
|
||||
createTime: string
|
||||
@@ -574,6 +590,7 @@ interface ToolTemplateAddEditParam {
|
||||
name?: string
|
||||
baseId?: string
|
||||
source?: string
|
||||
platform?: Platform
|
||||
entryPoint?: string
|
||||
enable?: boolean
|
||||
}
|
||||
@@ -583,6 +600,7 @@ interface ToolVo {
|
||||
name: string
|
||||
toolId: string
|
||||
icon: string
|
||||
platform: Platform
|
||||
description: string
|
||||
base: ToolBaseVo
|
||||
author: UserWithInfoVo
|
||||
@@ -596,12 +614,14 @@ interface ToolVo {
|
||||
review: 'NONE' | 'PROCESSING' | 'PASS' | 'REJECT'
|
||||
createTime: string
|
||||
updateTime: string
|
||||
favorite: boolean
|
||||
}
|
||||
|
||||
interface ToolCreateParam {
|
||||
name: string
|
||||
toolId: string
|
||||
icon: string
|
||||
platform: Platform
|
||||
description: string
|
||||
ver: string
|
||||
templateId: string
|
||||
@@ -612,6 +632,7 @@ interface ToolCreateParam {
|
||||
interface ToolUpgradeParam {
|
||||
toolId: string
|
||||
ver: string
|
||||
platform: Platform
|
||||
}
|
||||
|
||||
interface ToolUpdateParam {
|
||||
@@ -637,3 +658,18 @@ interface ToolManagementPassParam {
|
||||
interface ToolStoreGetParam extends PageParam {
|
||||
searchValue?: string
|
||||
}
|
||||
|
||||
interface ToolFavoriteAddRemoveParam {
|
||||
authorId: string
|
||||
toolId: string
|
||||
platform: Platform
|
||||
}
|
||||
|
||||
interface ToolMenuItem {
|
||||
icon: string
|
||||
toolName: string
|
||||
toolId: string
|
||||
authorUsername: string
|
||||
ver: string
|
||||
platform: Platform
|
||||
}
|
||||
|
||||
@@ -10,7 +10,10 @@ createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<AntdConfigProvider
|
||||
theme={{
|
||||
token: { colorPrimary: COLOR_MAIN, colorLinkHover: COLOR_MAIN },
|
||||
token: {
|
||||
colorPrimary: COLOR_MAIN,
|
||||
colorLinkHover: COLOR_MAIN
|
||||
},
|
||||
components: {
|
||||
Tree: {
|
||||
colorBgContainer: 'transparent'
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
PERMISSION_USER_NOT_FOUND,
|
||||
SYSTEM_INVALID_CAPTCHA_CODE
|
||||
} from '@/constants/common.constants'
|
||||
import { navigateToLogin } from '@/util/navigation'
|
||||
import { r_auth_forget, r_auth_retrieve } from '@/services/auth'
|
||||
import FitCenter from '@/components/common/FitCenter'
|
||||
import FlexBox from '@/components/common/FlexBox'
|
||||
@@ -155,8 +156,8 @@ const Forget = () => {
|
||||
>
|
||||
<AntdInput
|
||||
prefix={<Icon component={IconOxygenEmail} />}
|
||||
placeholder={'邮箱'}
|
||||
disabled={isSending}
|
||||
placeholder={'邮箱'}
|
||||
/>
|
||||
</AntdForm.Item>
|
||||
<AntdForm.Item>
|
||||
@@ -199,6 +200,7 @@ const Forget = () => {
|
||||
name={'password'}
|
||||
rules={[
|
||||
{ required: true, message: '请输入密码' },
|
||||
{ whitespace: true, message: '密码不能为空字符' },
|
||||
{ min: 10, message: '密码至少为10位' },
|
||||
{ max: 30, message: '密码最多为30位' }
|
||||
]}
|
||||
@@ -208,8 +210,8 @@ const Forget = () => {
|
||||
addonBefore={
|
||||
<span>新 密 码</span>
|
||||
}
|
||||
placeholder={'密码'}
|
||||
disabled={isChanging}
|
||||
placeholder={'密码'}
|
||||
/>
|
||||
</AntdForm.Item>
|
||||
<AntdForm.Item
|
||||
@@ -234,8 +236,8 @@ const Forget = () => {
|
||||
<AntdInput.Password
|
||||
id={'forget-password-confirm'}
|
||||
addonBefore={'确认密码'}
|
||||
placeholder={'确认密码'}
|
||||
disabled={isChanging}
|
||||
placeholder={'确认密码'}
|
||||
/>
|
||||
</AntdForm.Item>
|
||||
<AntdForm.Item>
|
||||
@@ -271,7 +273,15 @@ const Forget = () => {
|
||||
|
||||
<div className={'footer'}>
|
||||
找到了?
|
||||
<a onClick={() => navigate(`/login`, { replace: true })}>登录</a>
|
||||
<a
|
||||
onClick={() =>
|
||||
navigateToLogin(navigate, location.search, undefined, {
|
||||
replace: true
|
||||
})
|
||||
}
|
||||
>
|
||||
登录
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</FlexBox>
|
||||
|
||||