Compare commits
29 Commits
Author | SHA1 | Date |
---|---|---|
|
e969ec2236 | 1 day ago |
|
a9de1ec525 | 3 days ago |
|
fcad3681b4 | 4 weeks ago |
|
f19c097001 | 1 month ago |
|
81f76353f6 | 2 months ago |
|
068777d914 | 2 months ago |
|
34a762ad2c | 2 months ago |
|
23893adc43 | 2 months ago |
|
6692e16720 | 2 months ago |
|
de5f511d6a | 2 months ago |
|
d4be8b22b9 | 2 months ago |
|
c142937af9 | 2 months ago |
|
950bfe9060 | 2 months ago |
|
7035429775 | 2 months ago |
|
05f83e2a08 | 2 months ago |
|
fa6ef80493 | 2 months ago |
|
0f093b2ef9 | 2 months ago |
|
80cc4fe0fe | 2 months ago |
|
28eea56a3d | 2 months ago |
|
b6964f5fbe | 2 months ago |
|
7246ab2d9a | 2 months ago |
|
bd9ac214c6 | 2 months ago |
|
dcdc4aa857 | 2 months ago |
|
2d5a57853d | 2 months ago |
|
3c434df31c | 2 months ago |
|
b4b975174d | 3 months ago |
|
91f06eb4a1 | 3 months ago |
|
248716be69 | 3 months ago |
|
ca363ceac9 | 3 months ago |
141 changed files with 8859 additions and 3692 deletions
@ -1,34 +1,23 @@ |
|||
const { readFileSync } = require("node:fs") |
|||
|
|||
/* eslint-env node */ |
|||
require("@rushstack/eslint-patch/modern-module-resolution") |
|||
|
|||
const prettierConfig = JSON.parse(readFileSync("./.prettierrc", { encoding: "utf-8" })) |
|||
|
|||
module.exports = { |
|||
extends: [ |
|||
"eslint:recommended", |
|||
"plugin:vue/vue3-recommended", |
|||
"@electron-toolkit", |
|||
"@electron-toolkit/eslint-config-ts/eslint-recommended", |
|||
"@vue/eslint-config-typescript/recommended", |
|||
"@vue/eslint-config-prettier", |
|||
], |
|||
rules: { |
|||
"vue/require-default-prop": "off", |
|||
"vue/multi-word-component-names": "off", |
|||
"@typescript-eslint/no-explicit-any": "off", |
|||
"prettier/prettier": [ |
|||
"error", |
|||
{ |
|||
tabWidth: 4, |
|||
useTabs: false, |
|||
semi: false, |
|||
singleQuote: false, |
|||
trailingComma: "all", |
|||
bracketSpacing: true, |
|||
arrowParens: "avoid", |
|||
printWidth: 140, |
|||
htmlWhitespaceSensitivity: "ignore", |
|||
proseWrap: "preserve", |
|||
endOfLine: "auto", |
|||
}, |
|||
], |
|||
}, |
|||
extends: [ |
|||
"eslint:recommended", |
|||
"plugin:vue/vue3-recommended", |
|||
"@electron-toolkit", |
|||
"@electron-toolkit/eslint-config-ts/eslint-recommended", |
|||
"@vue/eslint-config-typescript/recommended", |
|||
"@vue/eslint-config-prettier", |
|||
], |
|||
rules: { |
|||
"vue/require-default-prop": "off", |
|||
"vue/multi-word-component-names": "off", |
|||
"@typescript-eslint/no-explicit-any": "off", |
|||
"prettier/prettier": ["error", prettierConfig], |
|||
}, |
|||
} |
|||
|
@ -1,13 +1,32 @@ |
|||
{ |
|||
"tabWidth": 4, |
|||
"useTabs": false, |
|||
"semi": false, |
|||
"singleQuote": false, |
|||
"trailingComma": "all", |
|||
"bracketSpacing": true, |
|||
"arrowParens": "avoid", |
|||
"printWidth": 140, |
|||
"htmlWhitespaceSensitivity": "ignore", |
|||
"proseWrap": "preserve", |
|||
"endOfLine": "auto" |
|||
"tabWidth": 2, |
|||
"useTabs": false, |
|||
"semi": false, |
|||
"singleQuote": false, |
|||
"trailingComma": "all", |
|||
"bracketSpacing": true, |
|||
"arrowParens": "avoid", |
|||
"printWidth": 140, |
|||
"htmlWhitespaceSensitivity": "ignore", |
|||
"proseWrap": "preserve", |
|||
"endOfLine": "auto", |
|||
"vueIndentScriptAndStyle": true, |
|||
"embeddedLanguageFormatting": "auto", |
|||
"jsxSingleQuote": false, |
|||
"jsxBracketSameLine": false, |
|||
"quoteProps": "as-needed", |
|||
"overrides": [ |
|||
{ |
|||
"files": "*.json", |
|||
"options": { |
|||
"printWidth": 80 |
|||
} |
|||
}, |
|||
{ |
|||
"files": ["*.vue", "*.tsx"], |
|||
"options": { |
|||
"singleAttributePerLine": false |
|||
} |
|||
} |
|||
] |
|||
} |
|||
|
@ -1,3 +1,3 @@ |
|||
{ |
|||
"recommendations": ["dbaeumer.vscode-eslint"] |
|||
"recommendations": ["dbaeumer.vscode-eslint", "lokalise.i18n-ally"] |
|||
} |
|||
|
@ -1,39 +1,39 @@ |
|||
{ |
|||
"version": "0.2.0", |
|||
"configurations": [ |
|||
{ |
|||
"name": "Debug Main Process", |
|||
"type": "node", |
|||
"request": "launch", |
|||
"cwd": "${workspaceRoot}", |
|||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite", |
|||
"windows": { |
|||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd" |
|||
}, |
|||
"runtimeArgs": ["--sourcemap"], |
|||
"env": { |
|||
"REMOTE_DEBUGGING_PORT": "9222" |
|||
} |
|||
}, |
|||
{ |
|||
"name": "Debug Renderer Process", |
|||
"port": 9222, |
|||
"request": "attach", |
|||
"type": "chrome", |
|||
"webRoot": "${workspaceFolder}/src/renderer", |
|||
"timeout": 60000, |
|||
"presentation": { |
|||
"hidden": true |
|||
} |
|||
} |
|||
], |
|||
"compounds": [ |
|||
{ |
|||
"name": "Debug All", |
|||
"configurations": ["Debug Main Process", "Debug Renderer Process"], |
|||
"presentation": { |
|||
"order": 1 |
|||
} |
|||
} |
|||
] |
|||
"version": "0.2.0", |
|||
"configurations": [ |
|||
{ |
|||
"name": "Debug Main Process", |
|||
"type": "node", |
|||
"request": "launch", |
|||
"cwd": "${workspaceRoot}", |
|||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite", |
|||
"windows": { |
|||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd" |
|||
}, |
|||
"runtimeArgs": ["--sourcemap"], |
|||
"env": { |
|||
"REMOTE_DEBUGGING_PORT": "9222" |
|||
} |
|||
}, |
|||
{ |
|||
"name": "Debug Renderer Process", |
|||
"port": 9222, |
|||
"request": "attach", |
|||
"type": "chrome", |
|||
"webRoot": "${workspaceFolder}/src/renderer", |
|||
"timeout": 60000, |
|||
"presentation": { |
|||
"hidden": true |
|||
} |
|||
} |
|||
], |
|||
"compounds": [ |
|||
{ |
|||
"name": "Debug All", |
|||
"configurations": ["Debug Main Process", "Debug Renderer Process"], |
|||
"presentation": { |
|||
"order": 1 |
|||
} |
|||
} |
|||
] |
|||
} |
|||
|
@ -1,11 +1,25 @@ |
|||
{ |
|||
"[typescript]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
"[javascript]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
"[json]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
} |
|||
"[typescript]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
"[javascript]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
"[json]": { |
|||
"editor.defaultFormatter": "esbenp.prettier-vscode" |
|||
}, |
|||
"i18n-ally.localesPaths": ["packages/locales/languages"], |
|||
"i18n-ally.sourceLanguage": "zh", |
|||
"i18n-ally.displayLanguage": "zh", |
|||
"i18n-ally.keystyle": "nested", |
|||
"i18n-ally.extract.autoDetect": true, |
|||
"i18n-ally.enabledFrameworks": ["vue"], |
|||
"i18n-ally.enabledParsers": ["json"], |
|||
"i18n-ally.translate.engines":[ |
|||
"openai" |
|||
], |
|||
// "i18n-ally.translate.openai.apiKey": "", 自己配置,防止泄露 |
|||
"i18n-ally.translate.openai.apiModel": "Qwen/Qwen2.5-72B-Instruct", |
|||
"i18n-ally.translate.openai.apiRoot": "https://api.siliconflow.cn", |
|||
"i18n-ally.translate.overrideExisting": true |
|||
} |
|||
|
@ -1,35 +1,52 @@ |
|||
import { LogLevel } from "logger/common" |
|||
// 定义主题类型
|
|||
type ThemeType = "light" | "dark" | "auto" |
|||
// 定义语言类型
|
|||
type LanguageType = "zh" | "en" |
|||
// 定义编辑器logo类型
|
|||
type LogoType = "logo" | "bg" |
|||
|
|||
// 配置接口定义
|
|||
export interface IDefaultConfig { |
|||
language: LanguageType |
|||
"common.theme": ThemeType |
|||
debug: LogLevel |
|||
"desktop:wallpaper": string |
|||
"update.hoturl": string |
|||
"update.repo"?: string |
|||
"update.owner"?: string |
|||
"update.allowDowngrade": boolean |
|||
"update.allowPrerelease": boolean |
|||
"editor.bg": string |
|||
"editor.logoType": LogoType |
|||
"editor.fontFamily": string |
|||
"snippet.storagePath": string |
|||
storagePath: string |
|||
} |
|||
|
|||
interface IConfig { |
|||
app_title: string |
|||
default_config: { |
|||
language: "zh" | "en" // i18n
|
|||
"common.theme": "light" | "dark" | "auto" // 主题
|
|||
"desktop:wallpaper": string |
|||
"update.repo"?: string // 更新地址
|
|||
"update.owner"?: string // 更新通道
|
|||
"update.allowDowngrade": boolean |
|||
"update.allowPrerelease": boolean |
|||
"editor.bg": string // 更新通道
|
|||
"editor.logoType": "logo" | "bg" // 更新通道
|
|||
"editor.fontFamily": string // 更新通道
|
|||
// "snippet.storagePath": string // 代码片段保存位置
|
|||
// "bookmark.storagePath": string // 书签保存位置
|
|||
// backup_rule: string // 备份规则
|
|||
storagePath: string // 存储地址
|
|||
} |
|||
app_title: string |
|||
default_config: IDefaultConfig |
|||
} |
|||
|
|||
// 默认配置导出
|
|||
export default { |
|||
app_title: "zephyr", // 和风
|
|||
default_config: { |
|||
storagePath: "$storagePath$", |
|||
language: "zh", |
|||
"common.theme": "auto", |
|||
"desktop:wallpaper": "", |
|||
"editor.bg": "", |
|||
"editor.logoType": "logo", |
|||
"editor.fontFamily": "Cascadia Mono, Consolas, 'Courier New', monospace", |
|||
"update.repo": "wood-desktop", |
|||
"update.owner": "npmrun", |
|||
"update.allowDowngrade": false, |
|||
"update.allowPrerelease": false, |
|||
}, |
|||
} as IConfig |
|||
app_title: "zephyr", // 和风
|
|||
default_config: { |
|||
storagePath: "$storagePath$", |
|||
language: "zh", |
|||
debug: LogLevel.INFO, |
|||
"common.theme": "auto", |
|||
"desktop:wallpaper": "", |
|||
"editor.bg": "", |
|||
"editor.logoType": "logo", |
|||
"editor.fontFamily": "Cascadia Mono, Consolas, 'Courier New', monospace", |
|||
"update.hoturl": |
|||
"https://alist.xieyaxin.top/d/%E8%B5%84%E6%BA%90/%E6%B5%8B%E8%AF%95%E6%96%87%E4%BB%B6.zip?sign=eqy35CR-J1SOQZz0iUN2P3B0BiyZPdYH0362nLXbUhE=:1749085071", |
|||
"update.repo": "wood-desktop", |
|||
"update.owner": "npmrun", |
|||
"update.allowDowngrade": false, |
|||
"update.allowPrerelease": false, |
|||
"snippet.storagePath": "$storagePath$/snippets", |
|||
}, |
|||
} as const satisfies IConfig |
|||
|
@ -1,45 +1,45 @@ |
|||
appId: com.zephyr.app |
|||
productName: zephyr |
|||
directories: |
|||
buildResources: build |
|||
buildResources: build |
|||
files: |
|||
- "!**/.vscode/*" |
|||
- "!src/*" |
|||
- "!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}" |
|||
- "!**/.vscode/*" |
|||
- "!src/*" |
|||
- "!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}" |
|||
asarUnpack: |
|||
- resources/** |
|||
- resources/** |
|||
win: |
|||
executableName: zephyr |
|||
executableName: zephyr |
|||
nsis: |
|||
artifactName: ${name}-${version}-setup.${ext} |
|||
shortcutName: ${productName} |
|||
uninstallDisplayName: ${productName} |
|||
createDesktopShortcut: always |
|||
artifactName: ${name}-${version}-setup.${ext} |
|||
shortcutName: ${productName} |
|||
uninstallDisplayName: ${productName} |
|||
createDesktopShortcut: always |
|||
mac: |
|||
entitlementsInherit: build/entitlements.mac.plist |
|||
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. |
|||
notarize: false |
|||
entitlementsInherit: build/entitlements.mac.plist |
|||
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. |
|||
notarize: false |
|||
dmg: |
|||
artifactName: ${name}-${version}.${ext} |
|||
artifactName: ${name}-${version}.${ext} |
|||
linux: |
|||
target: |
|||
- AppImage |
|||
- snap |
|||
- deb |
|||
maintainer: electronjs.org |
|||
category: Utility |
|||
target: |
|||
- AppImage |
|||
- snap |
|||
- deb |
|||
maintainer: electronjs.org |
|||
category: Utility |
|||
appImage: |
|||
artifactName: ${name}-${version}.${ext} |
|||
artifactName: ${name}-${version}.${ext} |
|||
npmRebuild: false |
|||
publish: |
|||
provider: generic |
|||
url: https://example.com/auto-updates |
|||
provider: generic |
|||
url: https://example.com/auto-updates |
|||
electronDownload: |
|||
mirror: https://npmmirror.com/mirrors/electron/ |
|||
mirror: https://npmmirror.com/mirrors/electron/ |
|||
|
@ -1,71 +1,89 @@ |
|||
{ |
|||
"name": "zephyr", |
|||
"type": "module", |
|||
"private": true, |
|||
"version": "1.0.0", |
|||
"description": "An Electron application with Vue and TypeScript", |
|||
"main": "./out/main/index.js", |
|||
"author": "example.com", |
|||
"homepage": "https://electron-vite.org", |
|||
"scripts": { |
|||
"runInstall": "node node_modules/electron/install.js", |
|||
"format": "prettier --write .", |
|||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix", |
|||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", |
|||
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false", |
|||
"typecheck": "npm run typecheck:node && npm run typecheck:web", |
|||
"start": "electron-vite preview", |
|||
"dev": "chcp 65001 && set DEBUG=app:*&& electron-vite dev", |
|||
"dev:watch": "chcp 65001 & set DEBUG=app:*& electron-vite dev --watch", |
|||
"build": "npm run typecheck && electron-vite build", |
|||
"postinstall": "electron-builder install-app-deps", |
|||
"build:unpack": "npm run build && electron-builder --dir", |
|||
"build:win": "npm run build && electron-builder --win", |
|||
"build:mac": "npm run build && electron-builder --mac", |
|||
"build:linux": "npm run build && electron-builder --linux" |
|||
}, |
|||
"dependencies": { |
|||
"@electron-toolkit/preload": "^3.0.1", |
|||
"@electron-toolkit/utils": "^3.0.0", |
|||
"@types/debug": "^4.1.12", |
|||
"@unocss/reset": "^0.64.1", |
|||
"@vueuse/core": "^12.7.0", |
|||
"electron-updater": "^6.3.9", |
|||
"inversify": "^6.2.2", |
|||
"lowdb": "^7.0.1", |
|||
"reflect-metadata": "^0.2.2", |
|||
"sass": "^1.85.0", |
|||
"unplugin-auto-import": "^19.1.0", |
|||
"unplugin-vue-components": "^28.4.0", |
|||
"unplugin-vue-macros": "^2.14.2", |
|||
"unplugin-vue-router": "^0.11.2", |
|||
"vue-router": "^4.5.0" |
|||
}, |
|||
"devDependencies": { |
|||
"@electron-toolkit/eslint-config": "^1.0.2", |
|||
"@electron-toolkit/eslint-config-ts": "^2.0.0", |
|||
"@electron-toolkit/tsconfig": "^1.0.1", |
|||
"@rushstack/eslint-patch": "^1.10.5", |
|||
"@types/node": "^20.17.19", |
|||
"@unocss/preset-rem-to-px": "^0.64.1", |
|||
"@vitejs/plugin-vue": "^5.2.1", |
|||
"@vitejs/plugin-vue-jsx": "^4.1.1", |
|||
"@vue/eslint-config-prettier": "^9.0.0", |
|||
"@vue/eslint-config-typescript": "^13.0.0", |
|||
"debug": "^4.4.0", |
|||
"electron": "^31.7.7", |
|||
"electron-builder": "^24.13.3", |
|||
"electron-vite": "^2.3.0", |
|||
"eslint": "^8.57.1", |
|||
"eslint-plugin-vue": "^9.32.0", |
|||
"prettier": "^3.5.1", |
|||
"rotating-file-stream": "^3.2.6", |
|||
"simplebar-vue": "^2.4.0", |
|||
"typescript": "^5.7.3", |
|||
"unocss": "^0.64.1", |
|||
"vite": "^5.4.14", |
|||
"vite-plugin-vue-layouts": "^0.11.0", |
|||
"vue": "^3.5.13", |
|||
"vue-tsc": "^2.1.10" |
|||
} |
|||
"name": "zephyr", |
|||
"type": "module", |
|||
"private": true, |
|||
"version": "0.0.1", |
|||
"description": "An Electron application with Vue and TypeScript", |
|||
"main": "./out/main/index.js", |
|||
"author": "example.com", |
|||
"homepage": "https://electron-vite.org", |
|||
"scripts": { |
|||
"runInstall": "node node_modules/electron/install.js", |
|||
"format": "prettier --write .", |
|||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix", |
|||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", |
|||
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false", |
|||
"typecheck": "npm run typecheck:node && npm run typecheck:web", |
|||
"start": "electron-vite preview", |
|||
"dev": "chcp 65001 && set DEBUG=app:*&& electron-vite dev", |
|||
"dev:watch": "chcp 65001 & set DEBUG=app:*& electron-vite dev --watch", |
|||
"build": "npm run typecheck && electron-vite build", |
|||
"postinstall": "electron-builder install-app-deps", |
|||
"build:unpack": "npm run build && electron-builder --dir", |
|||
"build:win": "npm run build && electron-builder --win", |
|||
"build:mac": "npm run build && electron-builder --mac", |
|||
"build:linux": "npm run build && electron-builder --linux" |
|||
}, |
|||
"dependencies": { |
|||
"@electron-toolkit/preload": "^3.0.1", |
|||
"@electron-toolkit/utils": "^3.0.0", |
|||
"electron-updater": "^6.3.9", |
|||
"fs-extra": "^11.3.0", |
|||
"inversify": "^6.2.2", |
|||
"lowdb": "^7.0.1", |
|||
"reflect-metadata": "^0.2.2" |
|||
}, |
|||
"devDependencies": { |
|||
"@electron-toolkit/eslint-config": "^1.0.2", |
|||
"@electron-toolkit/eslint-config-ts": "^2.0.0", |
|||
"@electron-toolkit/tsconfig": "^1.0.1", |
|||
"@iconify/json": "^2.2.324", |
|||
"@intlify/unplugin-vue-i18n": "^6.0.3", |
|||
"@rushstack/eslint-patch": "^1.10.5", |
|||
"@types/debug": "^4.1.12", |
|||
"@types/node": "^20.17.19", |
|||
"@types/nprogress": "^0.2.3", |
|||
"@unocss/preset-rem-to-px": "^0.64.1", |
|||
"@unocss/reset": "^0.64.1", |
|||
"@vitejs/plugin-vue": "^5.2.1", |
|||
"@vitejs/plugin-vue-jsx": "^4.1.1", |
|||
"@vue/eslint-config-prettier": "^9.0.0", |
|||
"@vue/eslint-config-typescript": "^13.0.0", |
|||
"@vueuse/core": "^12.7.0", |
|||
"base": "workspace:*", |
|||
"debug": "^4.4.0", |
|||
"electron": "^31.7.7", |
|||
"electron-builder": "^24.13.3", |
|||
"electron-vite": "^2.3.0", |
|||
"eslint": "^8.57.1", |
|||
"eslint-plugin-vue": "^9.32.0", |
|||
"extract-zip": "^2.0.1", |
|||
"helper": "workspace:*", |
|||
"locales": "workspace:*", |
|||
"lodash-es": "^4.17.21", |
|||
"logger": "workspace:^", |
|||
"monaco-editor": "^0.52.2", |
|||
"nprogress": "^0.2.0", |
|||
"pinia": "^3.0.2", |
|||
"pinia-plugin-persistedstate": "^4.2.0", |
|||
"prettier": "^3.5.1", |
|||
"rotating-file-stream": "^3.2.6", |
|||
"sass": "^1.85.0", |
|||
"setting": "workspace:^", |
|||
"simplebar-vue": "^2.4.0", |
|||
"typescript": "^5.7.3", |
|||
"unocss": "^0.64.1", |
|||
"unplugin-auto-import": "^19.1.0", |
|||
"unplugin-icons": "^22.1.0", |
|||
"unplugin-vue-components": "^28.4.0", |
|||
"unplugin-vue-macros": "^2.14.2", |
|||
"unplugin-vue-router": "^0.11.2", |
|||
"vite": "^5.4.14", |
|||
"vite-plugin-monaco-editor": "^1.1.0", |
|||
"vite-plugin-vue-layouts": "^0.11.0", |
|||
"vue": "^3.5.13", |
|||
"vue-i18n": "^11.1.1", |
|||
"vue-router": "^4.5.0", |
|||
"vue-tsc": "^2.1.10" |
|||
} |
|||
} |
|||
|
@ -0,0 +1,123 @@ |
|||
type FireFN = (...argu: any[]) => void |
|||
|
|||
// 监听器类型定义,支持优先级
|
|||
interface Listener<F extends FireFN> { |
|||
fn: F |
|||
once: boolean |
|||
priority: number |
|||
} |
|||
|
|||
class FireEvent<T extends Record<string | symbol, FireFN>> { |
|||
// 使用 Map 存储事件监听器,支持 symbol 键
|
|||
private events = new Map<keyof T, Array<Listener<T[keyof T]>>>() |
|||
|
|||
// 获取事件监听器列表,如果不存在则创建
|
|||
private getListeners<S extends keyof T>(name: S): Array<Listener<T[S]>> { |
|||
if (!this.events.has(name)) { |
|||
this.events.set(name, []) |
|||
} |
|||
return this.events.get(name) as Array<Listener<T[S]>> |
|||
} |
|||
|
|||
// 按优先级排序监听器
|
|||
private sortListeners<S extends keyof T>(name: S) { |
|||
const listeners = this.getListeners(name) |
|||
listeners.sort((a, b) => b.priority - a.priority) |
|||
} |
|||
|
|||
// 打印事件和监听器信息
|
|||
print() { |
|||
console.log("Registered Events:") |
|||
this.events.forEach((listeners, name) => { |
|||
// 显式处理 symbol 类型
|
|||
const keyType = typeof name === "symbol" ? `Symbol(${name.description || ""})` : String(name) |
|||
console.log(` ${keyType}: ${listeners.length} listeners`) |
|||
}) |
|||
} |
|||
|
|||
// 添加事件监听器,支持优先级
|
|||
on<S extends keyof T>(name: S, fn: T[S], priority = 0): this { |
|||
const listeners = this.getListeners(name) |
|||
listeners.push({ fn, once: false, priority }) |
|||
this.sortListeners(name) |
|||
return this // 支持链式调用
|
|||
} |
|||
|
|||
// 触发事件
|
|||
emit<S extends keyof T>(name: S, ...args: Parameters<T[S]>): this { |
|||
const listeners = this.getListeners(name).slice() // 创建副本以避免移除时的问题
|
|||
|
|||
for (const { fn } of listeners) { |
|||
try { |
|||
fn(...args) |
|||
} catch (error) { |
|||
console.error(`Error in event handler for ${String(name)}:`, error) |
|||
} |
|||
} |
|||
|
|||
// 移除一次性监听器
|
|||
if (listeners.some(l => l.once)) { |
|||
this.events.set( |
|||
name, |
|||
this.getListeners(name).filter(l => !l.once), |
|||
) |
|||
} |
|||
|
|||
return this |
|||
} |
|||
|
|||
// 移除事件监听器
|
|||
off<S extends keyof T>(name: S, fn?: T[S]): this { |
|||
if (!this.events.has(name)) return this |
|||
|
|||
const listeners = this.getListeners(name) |
|||
|
|||
if (!fn) { |
|||
// 移除所有监听器
|
|||
this.events.delete(name) |
|||
} else { |
|||
// 移除特定监听器
|
|||
const filtered = listeners.filter(l => l.fn !== fn) |
|||
if (filtered.length === 0) { |
|||
this.events.delete(name) |
|||
} else { |
|||
this.events.set(name, filtered) |
|||
} |
|||
} |
|||
|
|||
return this |
|||
} |
|||
|
|||
// 添加一次性事件监听器
|
|||
once<S extends keyof T>(name: S, fn: T[S], priority = 0): this { |
|||
const listeners = this.getListeners(name) |
|||
listeners.push({ fn, once: true, priority }) |
|||
this.sortListeners(name) |
|||
return this |
|||
} |
|||
|
|||
// 清除所有事件监听器
|
|||
clear(): this { |
|||
this.events.clear() |
|||
return this |
|||
} |
|||
|
|||
// 获取指定事件的监听器数量
|
|||
listenerCount<S extends keyof T>(name: S): number { |
|||
return this.events.get(name)?.length || 0 |
|||
} |
|||
|
|||
// 检查事件是否有监听器
|
|||
hasListeners<S extends keyof T>(name: S): boolean { |
|||
return this.listenerCount(name) > 0 |
|||
} |
|||
|
|||
// 获取所有事件名称
|
|||
eventNames(): Array<keyof T> { |
|||
return Array.from(this.events.keys()) |
|||
} |
|||
} |
|||
|
|||
export function buildEmitter<T extends Record<string | symbol, FireFN>>() { |
|||
return new FireEvent<T>() |
|||
} |
@ -0,0 +1,51 @@ |
|||
// type FireKey = string
|
|||
type FireFN = (...argu: any[]) => void |
|||
|
|||
class FireEvent<T extends Record<string | symbol, FireFN>> { |
|||
#events: Record<keyof T, FireFN[]> = {} as any |
|||
print() { |
|||
Object.keys(this.#events).forEach(key => { |
|||
console.log(`${key}: ${this.#events[key]}\n`) |
|||
}) |
|||
} |
|||
on<S extends keyof T>(name: S, fn: T[S]) { |
|||
if (!this.#events[name]) { |
|||
this.#events[name] = [] |
|||
} |
|||
this.#events[name].push(fn) |
|||
} |
|||
emit<S extends keyof T>(name: S, ...argu: Parameters<T[S]>) { |
|||
if (this.#events[name]) { |
|||
this.#events[name].forEach(fn => { |
|||
fn(...argu) |
|||
}) |
|||
} |
|||
} |
|||
off<S extends keyof T>(name: S, fn?: T[S]) { |
|||
const len = this.#events[name].length |
|||
if (!len) { |
|||
return |
|||
} |
|||
if (!fn) { |
|||
this.#events[name] = [] |
|||
} else { |
|||
for (let i = len - 1; i >= 0; i--) { |
|||
const _fn = this.#events[name][i] |
|||
if (_fn === fn) { |
|||
this.#events[name].splice(i, 1) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
once<S extends keyof T>(name: S, fn: T[S]) { |
|||
const _fn: any = (...argu: any[]) => { |
|||
fn(...argu) |
|||
this.off<S>(name, _fn) |
|||
} |
|||
this.on(name, _fn) |
|||
} |
|||
} |
|||
|
|||
export function buildEmitter<T extends Record<string | symbol, FireFN>>() { |
|||
return new FireEvent<T>() |
|||
} |
@ -0,0 +1,27 @@ |
|||
// 抽象基类,使用泛型来正确推导子类类型
|
|||
abstract class BaseSingleton { |
|||
private static _instance: any |
|||
|
|||
public constructor() { |
|||
if (this.constructor === BaseSingleton) { |
|||
throw new Error("禁止直接实例化 BaseOne 抽象类") |
|||
} |
|||
|
|||
if ((this.constructor as any)._instance) { |
|||
throw new Error("构造函数私有化失败,禁止重复 new") |
|||
} |
|||
|
|||
// this.constructor 是子类,所以这里设为 instance
|
|||
;(this.constructor as any)._instance = this |
|||
} |
|||
|
|||
public static getInstance<T extends BaseSingleton>(this: new () => T): T { |
|||
const clazz = this as any as typeof BaseSingleton |
|||
if (!clazz._instance) { |
|||
clazz._instance = new this() |
|||
} |
|||
return clazz._instance as T |
|||
} |
|||
} |
|||
|
|||
export { BaseSingleton } |
@ -0,0 +1,13 @@ |
|||
{ |
|||
"name": "base", |
|||
"version": "1.0.0", |
|||
"description": "", |
|||
"main": "index.js", |
|||
"scripts": { |
|||
"test": "echo \"Error: no test specified\" && exit 1" |
|||
}, |
|||
"keywords": [], |
|||
"author": "", |
|||
"license": "ISC", |
|||
"packageManager": "pnpm@10.4.1" |
|||
} |
@ -0,0 +1,13 @@ |
|||
{ |
|||
"name": "helper", |
|||
"version": "1.0.0", |
|||
"description": "", |
|||
"main": "index.js", |
|||
"scripts": { |
|||
"test": "echo \"Error: no test specified\" && exit 1" |
|||
}, |
|||
"keywords": [], |
|||
"author": "", |
|||
"license": "ISC", |
|||
"packageManager": "pnpm@10.4.1" |
|||
} |
@ -0,0 +1,7 @@ |
|||
export const enum EventEnum { |
|||
UPDATE_PROGRESS = "update-progress", |
|||
} |
|||
|
|||
export type EventMaps = { |
|||
[EventEnum.UPDATE_PROGRESS]: () => void |
|||
} |
@ -0,0 +1,8 @@ |
|||
import { broadcast } from "main/utils" |
|||
import { EventEnum } from "../common" |
|||
|
|||
export { EventEnum } |
|||
|
|||
export function emit(key: EventEnum, ...args: any[]) { |
|||
broadcast(key, ...args) |
|||
} |
@ -0,0 +1,74 @@ |
|||
type DownloadPercent = { |
|||
url: string |
|||
option?: object |
|||
onprocess?: (now: number, all: number) => void |
|||
onsuccess?: (data: any) => void |
|||
onerror?: (res: Response) => void |
|||
} |
|||
const RequestPercent = async ({ |
|||
url = "", |
|||
option = { |
|||
headers: { |
|||
responseType: "arraybuffer", |
|||
}, |
|||
}, |
|||
onsuccess, |
|||
onerror, |
|||
onprocess, |
|||
}: DownloadPercent) => { |
|||
const response = (await fetch(url, option)) as any |
|||
if (!response.ok) { |
|||
onerror?.(response) |
|||
throw new Error(`下载失败`) |
|||
} |
|||
const reader = response?.body.getReader() |
|||
|
|||
// 文件总长度
|
|||
const contentLength = +response.headers.get("content-length") |
|||
|
|||
let receivedLength = 0 |
|||
const chunks: any[] = [] |
|||
// eslint-disable-next-line no-constant-condition
|
|||
while (true) { |
|||
const { done, value } = await reader.read() |
|||
|
|||
if (done) { |
|||
break |
|||
} |
|||
|
|||
chunks.push(value) |
|||
receivedLength += value.length |
|||
onprocess?.(receivedLength, contentLength) |
|||
} |
|||
// 这里的chunksAll 已经是ArrayBuffer的数据类型了,可以直接返回,也可以转为blob处理
|
|||
const chunksAll = new Uint8Array(receivedLength) |
|||
let position = 0 |
|||
for (const chunk of chunks) { |
|||
chunksAll.set(chunk, position) |
|||
position += chunk.length |
|||
} |
|||
|
|||
onsuccess?.(chunksAll) |
|||
|
|||
return chunksAll |
|||
} |
|||
|
|||
export { RequestPercent } |
|||
export default RequestPercent |
|||
|
|||
// RequestPercent({
|
|||
// url: "http://117.21.250.136:9812/ZxqyGateway/biz/file/downApk/%E6%98%93%E4%BC%81%E6%95%B0%E8%BD%AC%E5%B9%B3%E5%8F%B0app-1.2.7.apk",
|
|||
// option: {
|
|||
// headers: {
|
|||
// responseType: "arraybuffer",
|
|||
// },
|
|||
// },
|
|||
// onerror: () => {},
|
|||
// onsuccess: data => {
|
|||
// fs.writeFileSync("./aaa.apk", Buffer.from(data))
|
|||
// console.log("success", data)
|
|||
// },
|
|||
// onprocess: (receivedLength, contentLength) => {
|
|||
// console.log(receivedLength, contentLength)
|
|||
// },
|
|||
// })
|
@ -0,0 +1,118 @@ |
|||
import { spawn } from "node:child_process" |
|||
import fs from "node:fs" |
|||
import path from "node:path" |
|||
import os from "node:os" |
|||
import { app } from "electron" |
|||
import download from "./download" |
|||
import extract from "extract-zip" |
|||
|
|||
import _logger from "logger/main" |
|||
import { emit, EventEnum } from "../handler" |
|||
|
|||
const logger = _logger.createNamespace("hot-updater") |
|||
|
|||
function getUpdateScriptTemplate() { |
|||
return process.platform === "win32" |
|||
? ` |
|||
@echo off |
|||
timeout /t 2 |
|||
taskkill /IM "{{EXE_NAME}}" /F |
|||
xcopy /Y /E "{{UPDATE_DIR}}\\*" "{{APP_PATH}}" |
|||
start "" "{{EXE_PATH}}" |
|||
` |
|||
: ` |
|||
#!/bin/bash |
|||
sleep 2 |
|||
pkill -f "{{EXE_NAME}}" |
|||
cp -Rf "{{UPDATE_DIR}}/*" "{{APP_PATH}}/" |
|||
open "{{EXE_PATH}}" |
|||
` |
|||
} |
|||
|
|||
function generateUpdateScript() { |
|||
const scriptContent = getUpdateScriptTemplate() |
|||
.replace(/{{APP_PATH}}/g, process.platform === "win32" ? "%APP_PATH%" : "$APP_PATH") |
|||
.replace(/{{UPDATE_DIR}}/g, process.platform === "win32" ? "%UPDATE_DIR%" : "$UPDATE_DIR") |
|||
.replace(/{{EXE_PATH}}/g, process.platform === "win32" ? "%EXE_PATH%" : "$EXE_PATH") |
|||
.replace(/{{EXE_NAME}}/g, process.platform === "win32" ? "%EXE_NAME%" : "$EXE_NAME") |
|||
|
|||
const scriptPath = path.join(os.tmpdir(), `update.${process.platform === "win32" ? "bat" : "sh"}`) |
|||
fs.writeFileSync(scriptPath, scriptContent) |
|||
return scriptPath |
|||
} |
|||
// 标记是否需要热更新
|
|||
let shouldPerformHotUpdate = false |
|||
let isReadyUpdate = false |
|||
// 更新临时目录路径
|
|||
// 使用应用名称和随机字符串创建唯一的临时目录
|
|||
const updateTempDirPath = path.join(os.tmpdir(), `${app.getName()}-update-${Math.random().toString(36).substring(2, 15)}`) |
|||
app.once("will-quit", event => { |
|||
if (!shouldPerformHotUpdate) return |
|||
event.preventDefault() |
|||
const appPath = app.getAppPath() |
|||
const appExePath = process.execPath |
|||
const exeName = path.basename(appExePath) |
|||
// 生成动态脚本
|
|||
const scriptPath = generateUpdateScript() |
|||
|
|||
fs.chmodSync(scriptPath, 0o755) |
|||
|
|||
// 执行脚本
|
|||
const child = spawn(scriptPath, [], { |
|||
detached: true, |
|||
shell: true, |
|||
env: { |
|||
APP_PATH: appPath, |
|||
UPDATE_DIR: updateTempDirPath, |
|||
EXE_PATH: appExePath, |
|||
EXE_NAME: exeName, |
|||
}, |
|||
}) |
|||
child.unref() |
|||
app.exit() |
|||
}) |
|||
|
|||
// 下载热更新包
|
|||
export async function fetchHotUpdatePackage(updatePackageUrl: string) { |
|||
if (isReadyUpdate) return |
|||
|
|||
// 清除临时目录
|
|||
clearUpdateTempDir() |
|||
// 创建临时目录
|
|||
if (!fs.existsSync(updateTempDirPath)) { |
|||
fs.mkdirSync(updateTempDirPath, { recursive: true }) |
|||
} |
|||
|
|||
// 下载文件的本地保存路径
|
|||
const downloadPath = path.join(updateTempDirPath, "update.zip") |
|||
|
|||
try { |
|||
// 使用 fetch 下载更新包
|
|||
const arrayBuffer = await download({ |
|||
url: updatePackageUrl, |
|||
onprocess(now, all) { |
|||
logger.debug(`下载进度: ${((now / all) * 100).toFixed(2)}%`) |
|||
emit(EventEnum.UPDATE_PROGRESS, { percent: (now / all) * 100, now, all }) |
|||
}, |
|||
}) |
|||
fs.writeFileSync(downloadPath, Buffer.from(arrayBuffer)) |
|||
// 解压更新包
|
|||
await extract(downloadPath, { dir: updateTempDirPath }) |
|||
|
|||
// 删除下载的zip文件
|
|||
fs.unlinkSync(downloadPath) |
|||
isReadyUpdate = true |
|||
} catch (error) { |
|||
logger.debug("热更新包下载失败:", error) |
|||
throw error |
|||
} |
|||
} |
|||
|
|||
function clearUpdateTempDir() { |
|||
if (!fs.existsSync(updateTempDirPath)) return |
|||
fs.rmSync(updateTempDirPath, { recursive: true }) |
|||
} |
|||
|
|||
export function flagNeedUpdate() { |
|||
shouldPerformHotUpdate = true |
|||
} |
@ -0,0 +1,137 @@ |
|||
import pkg from "electron-updater" |
|||
import { app, dialog } from "electron" |
|||
import Setting from "setting/main" |
|||
import EventEmitter from "events" |
|||
import { BaseSingleton } from "base" |
|||
import { fetchHotUpdatePackage, flagNeedUpdate } from "./hot" |
|||
import Locales from "locales/main" |
|||
import _logger from "logger/main" |
|||
|
|||
const logger = _logger.createNamespace("updater") |
|||
const { autoUpdater } = pkg |
|||
|
|||
class _Updater extends BaseSingleton { |
|||
public events = new EventEmitter() |
|||
private timer: ReturnType<typeof setInterval> | null = null |
|||
// autoReplace = false
|
|||
async triggerHotUpdate(autoReplace = false) { |
|||
const url = Setting.values("update.hoturl") |
|||
await fetchHotUpdatePackage(url) |
|||
flagNeedUpdate() |
|||
if (!autoReplace) { |
|||
dialog.showMessageBox({ |
|||
title: Locales.t("update.ready.hot.title"), |
|||
message: Locales.t("update.ready.hot.desc", { version: app.getVersion() }), |
|||
}) |
|||
} else { |
|||
app.quit() |
|||
} |
|||
} |
|||
|
|||
constructor() { |
|||
super() |
|||
// 配置自动更新
|
|||
autoUpdater.autoDownload = false |
|||
autoUpdater.autoInstallOnAppQuit = true |
|||
|
|||
// 检查更新错误
|
|||
autoUpdater.on("error", error => { |
|||
logger.debug("Update error:", error) |
|||
}) |
|||
|
|||
// 检查更新
|
|||
autoUpdater.on("checking-for-update", () => { |
|||
logger.debug("Checking for updates...") |
|||
}) |
|||
|
|||
// 有可用更新
|
|||
autoUpdater.on("update-available", info => { |
|||
logger.debug("Update available:", info) |
|||
this.promptUserToUpdate() |
|||
}) |
|||
|
|||
// 没有可用更新
|
|||
autoUpdater.on("update-not-available", info => { |
|||
logger.debug("Update not available:", info) |
|||
}) |
|||
|
|||
// 更新下载进度
|
|||
autoUpdater.on("download-progress", progressObj => { |
|||
logger.debug( |
|||
`Download speed: ${progressObj.bytesPerSecond} - Downloaded ${progressObj.percent}% (${progressObj.transferred}/${progressObj.total})`, |
|||
) |
|||
}) |
|||
|
|||
// 更新下载完成
|
|||
autoUpdater.on("update-downloaded", info => { |
|||
logger.debug("Update downloaded:", info) |
|||
this.promptUserToInstall() |
|||
}) |
|||
} |
|||
|
|||
init() { |
|||
// 定期检查更新
|
|||
this.checkForUpdates() |
|||
this.timer && clearInterval(this.timer) |
|||
this.timer = setInterval( |
|||
() => { |
|||
this.checkForUpdates() |
|||
}, |
|||
1000 * 60 * 60, |
|||
) // 每小时检查一次
|
|||
} |
|||
|
|||
destroy() { |
|||
// 清理工作
|
|||
if (this.timer) { |
|||
clearInterval(this.timer) |
|||
this.timer = null |
|||
} |
|||
} |
|||
|
|||
private async checkForUpdates() { |
|||
if (app.isPackaged) { |
|||
try { |
|||
await autoUpdater.checkForUpdates() |
|||
logger.debug("Updater初始化检查成功.") |
|||
} catch (error) { |
|||
logger.debug("Failed to check for updates:", error) |
|||
} |
|||
} else { |
|||
logger.debug("正在开发模式,跳过更新检查.") |
|||
} |
|||
} |
|||
|
|||
private async promptUserToUpdate() { |
|||
const result = await dialog.showMessageBox({ |
|||
type: "info", |
|||
title: "发现新版本", |
|||
message: "是否下载新版本?", |
|||
buttons: ["下载", "暂不更新"], |
|||
defaultId: 0, |
|||
}) |
|||
|
|||
if (result.response === 0) { |
|||
autoUpdater.downloadUpdate() |
|||
} |
|||
} |
|||
|
|||
private async promptUserToInstall() { |
|||
const result = await dialog.showMessageBox({ |
|||
type: "info", |
|||
title: "更新已就绪", |
|||
message: "新版本已下载完成,是否立即安装?", |
|||
buttons: ["立即安装", "稍后安装"], |
|||
defaultId: 0, |
|||
}) |
|||
|
|||
if (result.response === 0) { |
|||
autoUpdater.quitAndInstall(false, true) |
|||
} |
|||
} |
|||
} |
|||
|
|||
const Updater = _Updater.getInstance() |
|||
|
|||
export { Updater } |
|||
export default Updater |
@ -0,0 +1,41 @@ |
|||
if (import.meta.env.DEV) { |
|||
// 引入之后可以热更新
|
|||
import("./languages/zh.json") |
|||
import("./languages/en.json") |
|||
} |
|||
|
|||
const datetimeFormats = { |
|||
en: { |
|||
short: { |
|||
year: "numeric", |
|||
month: "short", |
|||
day: "numeric", |
|||
}, |
|||
long: { |
|||
year: "numeric", |
|||
month: "short", |
|||
day: "numeric", |
|||
weekday: "short", |
|||
hour: "numeric", |
|||
minute: "numeric", |
|||
}, |
|||
}, |
|||
zh: { |
|||
short: { |
|||
year: "numeric", |
|||
month: "short", |
|||
day: "numeric", |
|||
}, |
|||
long: { |
|||
year: "numeric", |
|||
month: "short", |
|||
day: "numeric", |
|||
weekday: "short", |
|||
hour: "numeric", |
|||
minute: "numeric", |
|||
hour12: true, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
export { datetimeFormats } |
@ -0,0 +1,20 @@ |
|||
{ |
|||
"update": { |
|||
"ready": { |
|||
"hot": { |
|||
"desc": "The new version v{version} is ready for update and will be automatically updated the next time you launch the program.", |
|||
"title": "Prompt" |
|||
} |
|||
} |
|||
}, |
|||
"browser": { |
|||
"navbar": { |
|||
"menu": { |
|||
"fullscreen": "Full screen", |
|||
"quit-fullscreen": "Exit Full Screen", |
|||
"toggleDevTools": "Developer panel", |
|||
"label": "Menu" |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,20 @@ |
|||
{ |
|||
"update": { |
|||
"ready": { |
|||
"hot": { |
|||
"title": "提示", |
|||
"desc": "新版本 v{version} 已经准备好更新, 下次启动程序即可自动更新" |
|||
} |
|||
} |
|||
}, |
|||
"browser": { |
|||
"navbar": { |
|||
"menu": { |
|||
"label": "菜单", |
|||
"fullscreen": "全屏", |
|||
"quit-fullscreen": "取消全屏", |
|||
"toggleDevTools": "开发者面板" |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,53 @@ |
|||
import { app } from "electron" |
|||
import { get } from "lodash-es" |
|||
|
|||
import zh from "./languages/zh.json" |
|||
import en from "./languages/en.json" |
|||
|
|||
type FlattenObject<T, Prefix extends string = ""> = T extends object |
|||
? { |
|||
[K in keyof T & (string | number)]: FlattenObject<T[K], Prefix extends "" ? `${K}` : `${Prefix}.${K}`> |
|||
}[keyof T & (string | number)] |
|||
: Prefix |
|||
|
|||
type FlattenKeys<T> = FlattenObject<T> |
|||
|
|||
type TranslationKey = FlattenKeys<typeof zh> |
|||
|
|||
class Locale { |
|||
locale: string = "zh" |
|||
|
|||
constructor() { |
|||
try { |
|||
this.locale = app.getLocale() |
|||
} catch (e) { |
|||
console.log(e) |
|||
} |
|||
} |
|||
|
|||
isCN(): boolean { |
|||
return this.locale.startsWith("zh") |
|||
} |
|||
|
|||
t(key: TranslationKey, replacements?: Record<string, string>): string { |
|||
let text: string = this.isCN() ? get(zh, key) : get(en, key) |
|||
if (!text) { |
|||
text = get(zh, key) |
|||
if (!text) { |
|||
return key |
|||
} |
|||
} |
|||
if (replacements) { |
|||
// 替换所有形如 {key} 的占位符
|
|||
Object.entries(replacements).forEach(([key, value]) => { |
|||
console.log(text) |
|||
text = text.replace(new RegExp(`{${key}}`, "g"), value) |
|||
}) |
|||
} |
|||
return text |
|||
} |
|||
} |
|||
|
|||
const Locales = new Locale() |
|||
export default Locales |
|||
export { Locales } |
@ -0,0 +1,7 @@ |
|||
{ |
|||
"name": "locales", |
|||
"version": "1.0.0", |
|||
"keywords": [], |
|||
"author": "", |
|||
"license": "ISC" |
|||
} |
@ -0,0 +1,31 @@ |
|||
// 日志级别定义
|
|||
export enum LogLevel { |
|||
TRACE = 0, |
|||
DEBUG = 1, |
|||
INFO = 2, |
|||
WARN = 3, |
|||
ERROR = 4, |
|||
FATAL = 5, |
|||
OFF = 6, |
|||
} |
|||
// 日志级别名称映射
|
|||
export const LogLevelName: Record<LogLevel, string> = { |
|||
[LogLevel.TRACE]: "TRACE", |
|||
[LogLevel.DEBUG]: "DEBUG", |
|||
[LogLevel.INFO]: "INFO", |
|||
[LogLevel.WARN]: "WARN", |
|||
[LogLevel.ERROR]: "ERROR", |
|||
[LogLevel.FATAL]: "FATAL", |
|||
[LogLevel.OFF]: "OFF", |
|||
} |
|||
|
|||
// 日志颜色映射(控制台输出用)
|
|||
export const LogLevelColor: Record<LogLevel, string> = { |
|||
[LogLevel.TRACE]: "\x1b[90m", // 灰色
|
|||
[LogLevel.DEBUG]: "\x1b[36m", // 青色
|
|||
[LogLevel.INFO]: "\x1b[32m", // 绿色
|
|||
[LogLevel.WARN]: "\x1b[33m", // 黄色
|
|||
[LogLevel.ERROR]: "\x1b[31m", // 红色
|
|||
[LogLevel.FATAL]: "\x1b[35m", // 紫色
|
|||
[LogLevel.OFF]: "", // 无色
|
|||
} |
@ -0,0 +1,486 @@ |
|||
import { app, dialog } from "electron" |
|||
import fs from "fs" |
|||
import path from "path" |
|||
import os from "os" |
|||
import logger from "./main" |
|||
import errorHandler, { ErrorDetail } from "./main-error" |
|||
|
|||
/** |
|||
* 崩溃报告接口 |
|||
*/ |
|||
export interface CrashReport { |
|||
timestamp: string |
|||
error: ErrorDetail |
|||
systemInfo: { |
|||
platform: string |
|||
release: string |
|||
arch: string |
|||
totalMemory: number |
|||
freeMemory: number |
|||
uptime: number |
|||
} |
|||
appInfo: { |
|||
version: string |
|||
name: string |
|||
path: string |
|||
argv: string[] |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 崩溃处理配置 |
|||
*/ |
|||
export interface CrashHandlerOptions { |
|||
crashReportDir?: string |
|||
maxReports?: number |
|||
showDialog?: boolean |
|||
} |
|||
|
|||
/** |
|||
* 默认崩溃处理配置 |
|||
*/ |
|||
const DEFAULT_OPTIONS: CrashHandlerOptions = { |
|||
maxReports: 10, |
|||
showDialog: true, |
|||
} |
|||
|
|||
/** |
|||
* 崩溃处理类 |
|||
*/ |
|||
export class CrashHandler { |
|||
private static instance: CrashHandler |
|||
private options: CrashHandlerOptions |
|||
private crashReportDir: string |
|||
private initialized: boolean = false |
|||
private startTime: number = Date.now() |
|||
// private normalShutdown: boolean = false
|
|||
|
|||
/** |
|||
* 获取单例实例 |
|||
*/ |
|||
public static getInstance(): CrashHandler { |
|||
if (!CrashHandler.instance) { |
|||
CrashHandler.instance = new CrashHandler() |
|||
} |
|||
return CrashHandler.instance |
|||
} |
|||
|
|||
/** |
|||
* 构造函数 |
|||
*/ |
|||
private constructor() { |
|||
this.options = { ...DEFAULT_OPTIONS } |
|||
this.crashReportDir = path.join(app.getPath("userData"), "crash-reports") |
|||
} |
|||
|
|||
/** |
|||
* 初始化崩溃处理器 |
|||
*/ |
|||
public init(options?: CrashHandlerOptions): void { |
|||
if (this.initialized) { |
|||
return |
|||
} |
|||
|
|||
this.options = { ...this.options, ...options } |
|||
|
|||
if (options?.crashReportDir) { |
|||
this.crashReportDir = options.crashReportDir |
|||
} |
|||
|
|||
// 确保崩溃报告目录存在
|
|||
if (!fs.existsSync(this.crashReportDir)) { |
|||
fs.mkdirSync(this.crashReportDir, { recursive: true }) |
|||
} |
|||
|
|||
// 检查上次是否崩溃
|
|||
this.checkPreviousCrash() |
|||
|
|||
// 记录应用启动时间
|
|||
this.startTime = Date.now() |
|||
this.saveStartupMarker() |
|||
|
|||
// 设置全局未捕获异常处理器
|
|||
this.setupGlobalHandlers() |
|||
|
|||
// 设置应用退出处理
|
|||
app.on("before-quit", () => { |
|||
// this.normalShutdown = true
|
|||
this.clearStartupMarker() |
|||
}) |
|||
|
|||
this.initialized = true |
|||
logger.info("crash-handler", "Crash handler initialized") |
|||
} |
|||
|
|||
/** |
|||
* 设置全局未捕获异常处理器 |
|||
*/ |
|||
private setupGlobalHandlers(): void { |
|||
// 增强现有的错误处理器
|
|||
const originalCaptureError = errorHandler.captureError.bind(errorHandler) |
|||
errorHandler.captureError = (error: any, componentInfo?: string, additionalInfo?: Record<string, any>) => { |
|||
// 调用原始方法记录错误
|
|||
originalCaptureError(error, componentInfo, additionalInfo) |
|||
|
|||
// 对于严重错误,生成崩溃报告
|
|||
if (error instanceof Error && error.stack) { |
|||
this.generateCrashReport(error, componentInfo, additionalInfo) |
|||
} |
|||
} |
|||
|
|||
// 捕获未处理的Promise异常
|
|||
process.on("unhandledRejection", (reason, promise) => { |
|||
logger.error("crash-handler", `Unhandled Promise Rejection: ${reason}`) |
|||
this.generateCrashReport(reason, "unhandledRejection", { promise: String(promise) }) |
|||
}) |
|||
|
|||
// 捕获未捕获的异常
|
|||
process.on("uncaughtException", error => { |
|||
logger.error("crash-handler", `Uncaught Exception: ${error.message}`) |
|||
this.generateCrashReport(error, "uncaughtException") |
|||
|
|||
// 显示错误对话框
|
|||
if (this.options.showDialog) { |
|||
this.showCrashDialog(error) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* 生成崩溃报告 |
|||
*/ |
|||
private generateCrashReport(error: any, componentInfo?: string, additionalInfo?: Record<string, any>): void { |
|||
try { |
|||
// 格式化错误信息
|
|||
const errorDetail = this.formatError(error, componentInfo, additionalInfo) |
|||
|
|||
// 创建崩溃报告
|
|||
const crashReport: CrashReport = { |
|||
timestamp: new Date().toISOString(), |
|||
error: errorDetail, |
|||
systemInfo: { |
|||
platform: os.platform(), |
|||
release: os.release(), |
|||
arch: os.arch(), |
|||
totalMemory: os.totalmem(), |
|||
freeMemory: os.freemem(), |
|||
uptime: os.uptime(), |
|||
}, |
|||
appInfo: { |
|||
version: app.getVersion(), |
|||
name: app.getName(), |
|||
path: app.getAppPath(), |
|||
argv: process.argv, |
|||
}, |
|||
} |
|||
|
|||
// 保存崩溃报告
|
|||
this.saveCrashReport(crashReport) |
|||
} catch (e) { |
|||
logger.error("crash-handler", `Failed to generate crash report: ${e}`) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 格式化错误信息 |
|||
*/ |
|||
private formatError(error: any, componentInfo?: string, additionalInfo?: Record<string, any>): ErrorDetail { |
|||
// 基本错误信息
|
|||
const errorDetail: ErrorDetail = { |
|||
message: "", |
|||
timestamp: new Date().toISOString(), |
|||
type: "Unknown", |
|||
} |
|||
|
|||
// 处理不同类型的错误
|
|||
if (error instanceof Error) { |
|||
errorDetail.message = error.message |
|||
errorDetail.type = error.name || error.constructor.name |
|||
errorDetail.stack = error.stack |
|||
} else if (typeof error === "string") { |
|||
errorDetail.message = error |
|||
errorDetail.type = "String" |
|||
} else if (error === null) { |
|||
errorDetail.message = "Null error received" |
|||
errorDetail.type = "Null" |
|||
} else if (error === undefined) { |
|||
errorDetail.message = "Undefined error received" |
|||
errorDetail.type = "Undefined" |
|||
} else if (typeof error === "object") { |
|||
try { |
|||
errorDetail.message = error.message || JSON.stringify(error) |
|||
errorDetail.type = "Object" |
|||
errorDetail.additionalInfo = { ...error } |
|||
} catch (e) { |
|||
errorDetail.message = "Unserializable error object" |
|||
errorDetail.type = "Unserializable" |
|||
} |
|||
} else { |
|||
try { |
|||
errorDetail.message = String(error) |
|||
errorDetail.type = typeof error |
|||
} catch (e) { |
|||
errorDetail.message = "Error converting to string" |
|||
errorDetail.type = "Unknown" |
|||
} |
|||
} |
|||
|
|||
// 添加组件信息
|
|||
if (componentInfo) { |
|||
errorDetail.componentInfo = componentInfo |
|||
} |
|||
|
|||
// 添加额外信息
|
|||
if (additionalInfo) { |
|||
errorDetail.additionalInfo = { |
|||
...errorDetail.additionalInfo, |
|||
...additionalInfo, |
|||
} |
|||
} |
|||
|
|||
return errorDetail |
|||
} |
|||
|
|||
/** |
|||
* 保存崩溃报告 |
|||
*/ |
|||
private saveCrashReport(report: CrashReport): void { |
|||
try { |
|||
// 生成唯一的崩溃报告文件名
|
|||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-") |
|||
const filename = `crash-${timestamp}.json` |
|||
const filepath = path.join(this.crashReportDir, filename) |
|||
|
|||
// 写入崩溃报告
|
|||
fs.writeFileSync(filepath, JSON.stringify(report, null, 2)) |
|||
logger.info("crash-handler", `Crash report saved: ${filepath}`) |
|||
|
|||
// 清理旧的崩溃报告
|
|||
this.cleanupOldReports() |
|||
} catch (e) { |
|||
logger.error("crash-handler", `Failed to save crash report: ${e}`) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 清理旧的崩溃报告 |
|||
*/ |
|||
private cleanupOldReports(): void { |
|||
try { |
|||
// 获取所有崩溃报告文件
|
|||
const files = fs |
|||
.readdirSync(this.crashReportDir) |
|||
.filter(file => file.startsWith("crash-") && file.endsWith(".json")) |
|||
.map(file => ({ |
|||
name: file, |
|||
path: path.join(this.crashReportDir, file), |
|||
time: fs.statSync(path.join(this.crashReportDir, file)).mtime.getTime(), |
|||
})) |
|||
.sort((a, b) => b.time - a.time) // 按时间降序排序
|
|||
|
|||
// 删除超出最大数量的旧报告
|
|||
if (files.length > this.options.maxReports!) { |
|||
const filesToDelete = files.slice(this.options.maxReports!) |
|||
filesToDelete.forEach(file => { |
|||
fs.unlinkSync(file.path) |
|||
logger.debug("crash-handler", `Deleted old crash report: ${file.name}`) |
|||
}) |
|||
} |
|||
} catch (e) { |
|||
logger.error("crash-handler", `Failed to cleanup old reports: ${e}`) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 保存启动标记 |
|||
*/ |
|||
private saveStartupMarker(): void { |
|||
try { |
|||
const markerPath = path.join(this.crashReportDir, "startup-marker.json") |
|||
const marker = { |
|||
startTime: this.startTime, |
|||
pid: process.pid, |
|||
} |
|||
fs.writeFileSync(markerPath, JSON.stringify(marker)) |
|||
} catch (e) { |
|||
logger.error("crash-handler", `Failed to save startup marker: ${e}`) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 清除启动标记 |
|||
*/ |
|||
private clearStartupMarker(): void { |
|||
try { |
|||
const markerPath = path.join(this.crashReportDir, "startup-marker.json") |
|||
if (fs.existsSync(markerPath)) { |
|||
fs.unlinkSync(markerPath) |
|||
} |
|||
} catch (e) { |
|||
logger.error("crash-handler", `Failed to clear startup marker: ${e}`) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 检查上次是否崩溃 |
|||
*/ |
|||
private checkPreviousCrash(): boolean { |
|||
try { |
|||
const markerPath = path.join(this.crashReportDir, "startup-marker.json") |
|||
// 如果存在启动标记,说明上次可能崩溃了
|
|||
if (fs.existsSync(markerPath)) { |
|||
const markerData = JSON.parse(fs.readFileSync(markerPath, "utf8")) |
|||
const lastStartTime = markerData.startTime |
|||
const lastPid = markerData.pid |
|||
|
|||
logger.warn( |
|||
"crash-handler", |
|||
`Found previous startup marker. App may have crashed. Last PID: ${lastPid}, Last start time: ${new Date(lastStartTime).toISOString()}`, |
|||
) |
|||
|
|||
// 查找最近的崩溃报告
|
|||
const recentCrash = this.getRecentCrashReport() |
|||
|
|||
// 显示崩溃恢复对话框
|
|||
if (recentCrash && this.options.showDialog) { |
|||
app.whenReady().then(() => { |
|||
this.showRecoveryDialog(recentCrash) |
|||
}) |
|||
} |
|||
|
|||
// 清除旧的启动标记
|
|||
fs.unlinkSync(markerPath) |
|||
return true |
|||
} |
|||
} catch (e) { |
|||
logger.error("crash-handler", `Failed to check previous crash: ${e}`) |
|||
} |
|||
|
|||
return false |
|||
} |
|||
|
|||
/** |
|||
* 获取最近的崩溃报告 |
|||
*/ |
|||
private getRecentCrashReport(): CrashReport | null { |
|||
try { |
|||
// 获取所有崩溃报告文件
|
|||
const files = fs |
|||
.readdirSync(this.crashReportDir) |
|||
.filter(file => file.startsWith("crash-") && file.endsWith(".json")) |
|||
.map(file => ({ |
|||
name: file, |
|||
path: path.join(this.crashReportDir, file), |
|||
time: fs.statSync(path.join(this.crashReportDir, file)).mtime.getTime(), |
|||
})) |
|||
.sort((a, b) => b.time - a.time) // 按时间降序排序
|
|||
|
|||
// 读取最近的崩溃报告
|
|||
if (files.length > 0) { |
|||
const recentFile = files[0] |
|||
const reportData = fs.readFileSync(recentFile.path, "utf8") |
|||
return JSON.parse(reportData) as CrashReport |
|||
} |
|||
} catch (e) { |
|||
logger.error("crash-handler", `Failed to get recent crash report: ${e}`) |
|||
} |
|||
|
|||
return null |
|||
} |
|||
|
|||
/** |
|||
* 显示崩溃对话框 |
|||
*/ |
|||
private showCrashDialog(error: Error): void { |
|||
try { |
|||
const options = { |
|||
type: "error" as const, |
|||
title: "应用崩溃", |
|||
message: "应用遇到了一个严重错误,即将关闭", |
|||
detail: `错误信息: ${error.message}\n\n堆栈信息: ${error.stack}\n\n崩溃报告已保存,应用将在您点击确定后关闭。`, |
|||
buttons: ["确定"], |
|||
defaultId: 0, |
|||
} |
|||
|
|||
dialog.showMessageBoxSync(options) |
|||
|
|||
// 强制退出应用
|
|||
setTimeout(() => { |
|||
app.exit(1) |
|||
}, 1000) |
|||
} catch (e) { |
|||
logger.error("crash-handler", `Failed to show crash dialog: ${e}`) |
|||
app.exit(1) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 显示恢复对话框 |
|||
*/ |
|||
private showRecoveryDialog(crashReport: CrashReport): void { |
|||
try { |
|||
const crashTime = new Date(crashReport.timestamp).toLocaleString() |
|||
const errorMessage = crashReport.error.message |
|||
const errorType = crashReport.error.type |
|||
|
|||
const options = { |
|||
type: "warning" as const, |
|||
title: "应用恢复", |
|||
message: "应用上次异常退出", |
|||
detail: `应用在 ${crashTime} 因 ${errorType} 错误崩溃: ${errorMessage}\n\n崩溃报告已保存,您可以继续使用应用或联系开发者报告此问题。`, |
|||
buttons: ["继续", "查看详情"], |
|||
defaultId: 0, |
|||
} |
|||
|
|||
const response = dialog.showMessageBoxSync(options) |
|||
|
|||
// 如果用户选择查看详情
|
|||
if (response === 1) { |
|||
// 显示详细的崩溃报告
|
|||
this.showDetailedCrashInfo(crashReport) |
|||
} |
|||
} catch (e) { |
|||
logger.error("crash-handler", `Failed to show recovery dialog: ${e}`) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 显示详细的崩溃信息 |
|||
*/ |
|||
private showDetailedCrashInfo(crashReport: CrashReport): void { |
|||
try { |
|||
const options = { |
|||
type: "info" as const, |
|||
title: "崩溃详情", |
|||
message: "应用崩溃详细信息", |
|||
detail: JSON.stringify(crashReport, null, 2), |
|||
buttons: ["关闭"], |
|||
defaultId: 0, |
|||
} |
|||
|
|||
dialog.showMessageBoxSync(options) |
|||
} catch (e) { |
|||
logger.error("crash-handler", `Failed to show detailed crash info: ${e}`) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 设置崩溃处理选项 |
|||
*/ |
|||
public setOptions(options: Partial<CrashHandlerOptions>): void { |
|||
this.options = { ...this.options, ...options } |
|||
} |
|||
|
|||
/** |
|||
* 获取当前选项 |
|||
*/ |
|||
public getOptions(): CrashHandlerOptions { |
|||
return { ...this.options } |
|||
} |
|||
} |
|||
|
|||
// 创建默认实例
|
|||
const crashHandler = CrashHandler.getInstance() |
|||
|
|||
export default crashHandler |
|||
export { crashHandler } |
@ -0,0 +1,177 @@ |
|||
import { LogLevel, LogLevelName } from "./common" |
|||
import logger from "./main" |
|||
|
|||
/** |
|||
* 错误详情接口 |
|||
*/ |
|||
export interface ErrorDetail { |
|||
message: string |
|||
stack?: string |
|||
componentInfo?: string |
|||
additionalInfo?: Record<string, any> |
|||
timestamp: string |
|||
type: string |
|||
} |
|||
|
|||
/** |
|||
* 错误处理配置 |
|||
*/ |
|||
export interface ErrorHandlerOptions { |
|||
namespace?: string |
|||
level?: LogLevel |
|||
includeStack?: boolean |
|||
includeComponentInfo?: boolean |
|||
formatError?: (error: any) => ErrorDetail |
|||
} |
|||
|
|||
/** |
|||
* 默认错误处理配置 |
|||
*/ |
|||
const DEFAULT_OPTIONS: ErrorHandlerOptions = { |
|||
namespace: "error", |
|||
level: LogLevel.ERROR, |
|||
includeStack: true, |
|||
includeComponentInfo: true, |
|||
} |
|||
|
|||
/** |
|||
* 格式化错误信息 |
|||
*/ |
|||
const formatError = (error: any, options: ErrorHandlerOptions): ErrorDetail => { |
|||
// 如果已经是ErrorDetail格式,直接返回
|
|||
if (error && typeof error === "object" && error.type && error.message && error.timestamp) { |
|||
return error as ErrorDetail |
|||
} |
|||
|
|||
// 基本错误信息
|
|||
const errorDetail: ErrorDetail = { |
|||
message: "", |
|||
timestamp: new Date().toISOString(), |
|||
type: "Unknown", |
|||
} |
|||
|
|||
// 处理不同类型的错误
|
|||
if (error instanceof Error) { |
|||
errorDetail.message = error.message |
|||
errorDetail.type = error.name || error.constructor.name |
|||
if (options.includeStack) { |
|||
errorDetail.stack = error.stack |
|||
} |
|||
} else if (typeof error === "string") { |
|||
errorDetail.message = error |
|||
errorDetail.type = "String" |
|||
} else if (error === null) { |
|||
errorDetail.message = "Null error received" |
|||
errorDetail.type = "Null" |
|||
} else if (error === undefined) { |
|||
errorDetail.message = "Undefined error received" |
|||
errorDetail.type = "Undefined" |
|||
} else if (typeof error === "object") { |
|||
try { |
|||
errorDetail.message = error.message || JSON.stringify(error) |
|||
errorDetail.type = "Object" |
|||
errorDetail.additionalInfo = { ...error } |
|||
} catch (e) { |
|||
errorDetail.message = "Unserializable error object" |
|||
errorDetail.type = "Unserializable" |
|||
} |
|||
} else { |
|||
try { |
|||
errorDetail.message = String(error) |
|||
errorDetail.type = typeof error |
|||
} catch (e) { |
|||
errorDetail.message = "Error converting to string" |
|||
errorDetail.type = "Unknown" |
|||
} |
|||
} |
|||
|
|||
return errorDetail |
|||
} |
|||
|
|||
/** |
|||
* 错误处理类 |
|||
*/ |
|||
export class ErrorHandler { |
|||
private options: ErrorHandlerOptions |
|||
|
|||
constructor(options?: Partial<ErrorHandlerOptions>) { |
|||
this.options = { ...DEFAULT_OPTIONS, ...options } |
|||
} |
|||
|
|||
/** |
|||
* 记录错误 |
|||
*/ |
|||
public captureError(error: any, componentInfo?: string, additionalInfo?: Record<string, any>): void { |
|||
const errorDetail = formatError(error, this.options) |
|||
|
|||
// 添加组件信息
|
|||
if (this.options.includeComponentInfo && componentInfo) { |
|||
errorDetail.componentInfo = componentInfo |
|||
} |
|||
|
|||
// 添加额外信息
|
|||
if (additionalInfo) { |
|||
errorDetail.additionalInfo = { |
|||
...errorDetail.additionalInfo, |
|||
...additionalInfo, |
|||
} |
|||
} |
|||
|
|||
// 使用logger记录错误
|
|||
const namespace = this.options.namespace || "error" |
|||
const level = LogLevelName[this.options.level || LogLevel.ERROR].toLowerCase() |
|||
|
|||
// 构建错误消息
|
|||
let errorMessage = `${errorDetail.type}: ${errorDetail.message}` |
|||
if (errorDetail.componentInfo) { |
|||
errorMessage += ` | Component: ${errorDetail.componentInfo}` |
|||
} |
|||
|
|||
// 记录错误
|
|||
logger[level](namespace, errorMessage) |
|||
|
|||
// 如果有堆栈信息,单独记录
|
|||
if (errorDetail.stack) { |
|||
logger[level](namespace, `Stack: ${errorDetail.stack}`) |
|||
} |
|||
|
|||
// 如果有额外信息,单独记录
|
|||
if (errorDetail.additionalInfo) { |
|||
try { |
|||
const additionalInfoStr = JSON.stringify(errorDetail.additionalInfo, null, 2) |
|||
logger[level](namespace, `Additional Info: ${additionalInfoStr}`) |
|||
} catch (e) { |
|||
logger[level](namespace, "Additional Info: [Unserializable]") |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 设置错误处理选项 |
|||
*/ |
|||
public setOptions(options: Partial<ErrorHandlerOptions>): void { |
|||
this.options = { ...this.options, ...options } |
|||
} |
|||
|
|||
/** |
|||
* 获取当前选项 |
|||
*/ |
|||
public getOptions(): ErrorHandlerOptions { |
|||
return { ...this.options } |
|||
} |
|||
} |
|||
|
|||
// 创建默认实例
|
|||
const errorHandler = new ErrorHandler() |
|||
|
|||
// 捕获未处理的Promise异常
|
|||
process.on("unhandledRejection", reason => { |
|||
errorHandler.captureError(reason) |
|||
}) |
|||
|
|||
// 捕获未捕获的异常
|
|||
process.on("uncaughtException", error => { |
|||
errorHandler.captureError(error) |
|||
}) |
|||
|
|||
export default errorHandler |
@ -0,0 +1,275 @@ |
|||
import { app, ipcMain } from "electron" |
|||
import fs from "fs" |
|||
import path from "path" |
|||
import config from "config" |
|||
import * as rfs from "rotating-file-stream" |
|||
import { LogLevel, LogLevelColor, LogLevelName } from "./common" |
|||
import { emitter } from "setting/main/event" |
|||
|
|||
// 重置颜色的ANSI代码
|
|||
const RESET_COLOR = "\x1b[0m" |
|||
|
|||
// 日志配置接口
|
|||
export interface LoggerOptions { |
|||
level?: LogLevel // 日志级别
|
|||
namespace?: string // 日志命名空间
|
|||
console?: boolean // 是否输出到控制台
|
|||
file?: boolean // 是否输出到文件
|
|||
maxSize?: string // 单个日志文件最大大小
|
|||
maxFiles?: number // 保留的最大日志文件数量
|
|||
} |
|||
|
|||
// 默认配置
|
|||
const DEFAULT_OPTIONS: LoggerOptions = { |
|||
level: config.default_config.debug, |
|||
namespace: "app", |
|||
console: true, |
|||
file: true, |
|||
maxSize: "10M", |
|||
maxFiles: 10, |
|||
} |
|||
|
|||
let logDir |
|||
const isElectronApp = !!process.versions.electron |
|||
if (isElectronApp && app) { |
|||
logDir = path.join(app.getPath("logs")) |
|||
} else { |
|||
// 非Electron环境下使用当前目录下的logs文件夹
|
|||
logDir = path.join(process.cwd(), "logs") |
|||
} |
|||
|
|||
/** |
|||
* 日志管理类 |
|||
*/ |
|||
export class Logger { |
|||
private static instance: Logger |
|||
private options: LoggerOptions = DEFAULT_OPTIONS |
|||
private logStream: rfs.RotatingFileStream | null = null |
|||
private logDir: string = logDir |
|||
private currentLogFile: string = "" |
|||
private isElectronApp: boolean = !!process.versions.electron |
|||
private callInitialize: boolean = false |
|||
|
|||
/** |
|||
* 获取单例实例 |
|||
*/ |
|||
public static getInstance(): Logger { |
|||
if (!Logger.instance) { |
|||
Logger.instance = new Logger() |
|||
} |
|||
if (Logger.instance.callInitialize) { |
|||
return Logger.instance |
|||
} else { |
|||
// 创建代理对象,确保只有在初始化后才能访问除init之外的方法
|
|||
const handler = { |
|||
get: function (target: any, prop: string) { |
|||
if (prop === "init") { |
|||
return target[prop] |
|||
} |
|||
if (!target.callInitialize) { |
|||
throw new Error(`Logger未初始化,不能调用${prop}方法,请先调用init()方法`) |
|||
} |
|||
return target[prop] |
|||
}, |
|||
} |
|||
Logger.instance = new Proxy(new Logger(), handler) |
|||
} |
|||
return Logger.instance |
|||
} |
|||
|
|||
/** |
|||
* 构造函数 |
|||
*/ |
|||
// private constructor() {}
|
|||
|
|||
public init(options?: LoggerOptions): void { |
|||
this.callInitialize = true |
|||
this.options = { ...this.options, ...options } |
|||
|
|||
// 确保日志目录存在
|
|||
if (!fs.existsSync(this.logDir)) { |
|||
fs.mkdirSync(this.logDir, { recursive: true }) |
|||
} |
|||
|
|||
// 初始化日志文件
|
|||
this.initLogFile() |
|||
|
|||
// 如果在主进程中,设置IPC监听器接收渲染进程的日志
|
|||
if (this.isElectronApp && process.type === "browser") { |
|||
this.setupIPC() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 初始化日志文件 |
|||
*/ |
|||
private initLogFile(): void { |
|||
if (!this.options.file) return |
|||
|
|||
// 生成日志文件名
|
|||
const now = new Date() |
|||
const timestamp = now.toISOString().replace(/[:.]/g, "-") |
|||
this.currentLogFile = `app-logger-${timestamp}.log` |
|||
|
|||
// 创建日志流
|
|||
this.logStream = rfs.createStream(this.currentLogFile, { |
|||
path: this.logDir, |
|||
size: this.options.maxSize, |
|||
rotate: this.options.maxFiles, |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* 设置IPC通信,接收渲染进程日志 |
|||
*/ |
|||
private setupIPC(): void { |
|||
if (!ipcMain) return |
|||
ipcMain.on("logger:log", (_, level: LogLevel, namespace: string, ...messages: any[]) => { |
|||
this.logWithLevel(level, namespace, ...messages) |
|||
}) |
|||
|
|||
// 处理日志级别设置请求
|
|||
ipcMain.on("logger:setLevel", (_, level: LogLevel) => { |
|||
this.setLevel(level) |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* 关闭日志流 |
|||
*/ |
|||
public close(): void { |
|||
if (this.logStream) { |
|||
this.logStream.end() |
|||
this.logStream.destroy() |
|||
this.logStream = null |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 设置日志级别 |
|||
*/ |
|||
public setLevel(level: LogLevel): void { |
|||
this.options.level = level |
|||
} |
|||
|
|||
/** |
|||
* 获取当前日志级别 |
|||
*/ |
|||
public getLevel(): LogLevel { |
|||
return this.options.level ?? LogLevel.INFO |
|||
} |
|||
|
|||
/** |
|||
* 根据级别记录日志 |
|||
*/ |
|||
private logWithLevel(level: LogLevel, namespace: string, ...messages: any[]): void { |
|||
// 检查日志级别
|
|||
if (level < this.getLevel() || level === LogLevel.OFF) return |
|||
|
|||
const timestamp = new Date().toISOString() |
|||
const levelName = LogLevelName[level] |
|||
const prefix = `[${timestamp}] [${namespace}] [${levelName}]` |
|||
|
|||
// 格式化消息
|
|||
const formattedMessages = messages.map(msg => { |
|||
if (typeof msg === "object") { |
|||
try { |
|||
return JSON.stringify(msg) |
|||
} catch (e) { |
|||
return String(msg) |
|||
} |
|||
} |
|||
return String(msg) |
|||
}) |
|||
|
|||
const message = formattedMessages.join(" ") |
|||
|
|||
// 输出到控制台
|
|||
if (this.options.console) { |
|||
const color = LogLevelColor[level] |
|||
console.log(`${color}${prefix} ${message}${RESET_COLOR}`) |
|||
} |
|||
|
|||
// 写入日志文件
|
|||
if (this.options.file && this.logStream) { |
|||
this.logStream.write(`${prefix} ${message}\n`) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 记录跟踪级别日志 |
|||
*/ |
|||
public trace(namespace: string, ...messages: any[]): void { |
|||
this.logWithLevel(LogLevel.TRACE, namespace, ...messages) |
|||
} |
|||
|
|||
/** |
|||
* 记录调试级别日志 |
|||
*/ |
|||
public debug(namespace: string, ...messages: any[]): void { |
|||
this.logWithLevel(LogLevel.DEBUG, namespace, ...messages) |
|||
} |
|||
|
|||
/** |
|||
* 记录信息级别日志 |
|||
*/ |
|||
public info(namespace: string, ...messages: any[]): void { |
|||
this.logWithLevel(LogLevel.INFO, namespace, ...messages) |
|||
} |
|||
|
|||
/** |
|||
* 记录警告级别日志 |
|||
*/ |
|||
public warn(namespace: string, ...messages: any[]): void { |
|||
this.logWithLevel(LogLevel.WARN, namespace, ...messages) |
|||
} |
|||
|
|||
/** |
|||
* 记录错误级别日志 |
|||
*/ |
|||
public error(namespace: string, ...messages: any[]): void { |
|||
this.logWithLevel(LogLevel.ERROR, namespace, ...messages) |
|||
} |
|||
|
|||
/** |
|||
* 记录致命错误级别日志 |
|||
*/ |
|||
public fatal(namespace: string, ...messages: any[]): void { |
|||
this.logWithLevel(LogLevel.FATAL, namespace, ...messages) |
|||
} |
|||
|
|||
/** |
|||
* 创建一个固定命名空间的日志记录器 |
|||
* @param namespace 命名空间 |
|||
* @returns 带有固定命名空间的日志记录器 |
|||
*/ |
|||
public createNamespace(namespace: string) { |
|||
return { |
|||
trace: (...messages: any[]) => this.trace(namespace, ...messages), |
|||
debug: (...messages: any[]) => this.debug(namespace, ...messages), |
|||
info: (...messages: any[]) => this.info(namespace, ...messages), |
|||
warn: (...messages: any[]) => this.warn(namespace, ...messages), |
|||
error: (...messages: any[]) => this.error(namespace, ...messages), |
|||
fatal: (...messages: any[]) => this.fatal(namespace, ...messages), |
|||
setLevel: (level: LogLevel) => this.setLevel(level), |
|||
getLevel: () => this.getLevel(), |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 默认实例
|
|||
const logger = Logger.getInstance() |
|||
logger.init() |
|||
emitter.on("update", setting => { |
|||
logger.setLevel(setting.debug) |
|||
}) |
|||
|
|||
// 应用退出时关闭日志流
|
|||
if (process.type === "browser" && app) { |
|||
app.on("before-quit", () => { |
|||
logger.info("app", "应用关闭") |
|||
logger.close() |
|||
}) |
|||
} |
|||
|
|||
export default logger |
@ -0,0 +1,7 @@ |
|||
{ |
|||
"name": "logger", |
|||
"version": "1.0.0", |
|||
"keywords": [], |
|||
"author": "", |
|||
"license": "ISC" |
|||
} |
@ -0,0 +1,195 @@ |
|||
import { contextBridge, ipcRenderer } from "electron" |
|||
import { LogLevel, LogLevelName } from "./common" |
|||
import logger from "./preload" |
|||
|
|||
/** |
|||
* 错误详情接口 |
|||
*/ |
|||
interface ErrorDetail { |
|||
message: string |
|||
stack?: string |
|||
componentInfo?: string |
|||
additionalInfo?: Record<string, any> |
|||
timestamp: string |
|||
type: string |
|||
} |
|||
|
|||
/** |
|||
* 错误处理配置 |
|||
*/ |
|||
interface ErrorHandlerOptions { |
|||
namespace?: string |
|||
level?: LogLevel |
|||
includeStack?: boolean |
|||
includeComponentInfo?: boolean |
|||
} |
|||
|
|||
/** |
|||
* 渲染进程错误处理接口 |
|||
*/ |
|||
interface IRendererErrorHandler { |
|||
/** |
|||
* 捕获错误 |
|||
*/ |
|||
captureError(error: any, componentInfo?: string, additionalInfo?: Record<string, any>): void |
|||
|
|||
/** |
|||
* 设置错误处理选项 |
|||
*/ |
|||
setOptions(options: Partial<ErrorHandlerOptions>): void |
|||
|
|||
/** |
|||
* 获取当前选项 |
|||
*/ |
|||
getOptions(): ErrorHandlerOptions |
|||
|
|||
/** |
|||
* 安装全局错误处理器 |
|||
*/ |
|||
installGlobalHandlers(): void |
|||
} |
|||
|
|||
/** |
|||
* 默认错误处理配置 |
|||
*/ |
|||
const DEFAULT_OPTIONS: ErrorHandlerOptions = { |
|||
namespace: "error", |
|||
level: LogLevel.ERROR, |
|||
includeStack: true, |
|||
includeComponentInfo: true, |
|||
} |
|||
|
|||
/** |
|||
* 格式化错误信息 |
|||
*/ |
|||
const formatError = (error: any, options: ErrorHandlerOptions): ErrorDetail => { |
|||
// 基本错误信息
|
|||
const errorDetail: ErrorDetail = { |
|||
message: "", |
|||
timestamp: new Date().toISOString(), |
|||
type: "Unknown", |
|||
} |
|||
// 处理不同类型的错误
|
|||
if (error instanceof Error) { |
|||
errorDetail.message = error.message |
|||
errorDetail.type = error.name || error.constructor.name |
|||
if (options.includeStack) { |
|||
errorDetail.stack = error.stack |
|||
} |
|||
} else if (typeof error === "string") { |
|||
errorDetail.message = error |
|||
errorDetail.type = "String" |
|||
} else if (error === null) { |
|||
errorDetail.message = "Null error received" |
|||
errorDetail.type = "Null" |
|||
} else if (error === undefined) { |
|||
errorDetail.message = "Undefined error received" |
|||
errorDetail.type = "Undefined" |
|||
} else if (typeof error === "object") { |
|||
try { |
|||
errorDetail.message = error.message || JSON.stringify(error) |
|||
errorDetail.type = "Object" |
|||
errorDetail.additionalInfo = { ...error } |
|||
} catch (e) { |
|||
errorDetail.message = "Unserializable error object" |
|||
errorDetail.type = "Unserializable" |
|||
} |
|||
} else { |
|||
try { |
|||
errorDetail.message = String(error) |
|||
errorDetail.type = typeof error |
|||
} catch (e) { |
|||
errorDetail.message = "Error converting to string" |
|||
errorDetail.type = "Unknown" |
|||
} |
|||
} |
|||
|
|||
return errorDetail |
|||
} |
|||
|
|||
/** |
|||
* 创建渲染进程错误处理器 |
|||
*/ |
|||
const createRendererErrorHandler = (): IRendererErrorHandler => { |
|||
// 当前错误处理选项
|
|||
let options: ErrorHandlerOptions = { ...DEFAULT_OPTIONS } |
|||
|
|||
/** |
|||
* 处理并转发错误到主进程 |
|||
*/ |
|||
const handleError = (error: any, componentInfo?: string, additionalInfo?: Record<string, any>) => { |
|||
// 如果已经是ErrorDetail格式,直接使用
|
|||
let errorDetail: ErrorDetail |
|||
if (error && typeof error === "object" && error.type && error.message && error.timestamp) { |
|||
errorDetail = error as ErrorDetail |
|||
} else { |
|||
// 否则格式化错误
|
|||
errorDetail = formatError(error, options) |
|||
} |
|||
|
|||
// 添加组件信息
|
|||
if (options.includeComponentInfo && componentInfo) { |
|||
errorDetail.componentInfo = componentInfo |
|||
} |
|||
|
|||
// 使用logger记录错误
|
|||
const namespace = options.namespace || "error" |
|||
const level = LogLevelName[options.level || LogLevel.ERROR].toLowerCase() |
|||
|
|||
// 添加额外信息
|
|||
if (additionalInfo) { |
|||
errorDetail.additionalInfo = { |
|||
...errorDetail.additionalInfo, |
|||
...additionalInfo, |
|||
} |
|||
} |
|||
|
|||
// 记录完整的错误信息
|
|||
logger[level](namespace, JSON.stringify(errorDetail)) |
|||
|
|||
// 同时在控制台输出错误信息
|
|||
logger[level](namespace, `${errorDetail.type}: ${errorDetail.message}`) |
|||
if (errorDetail.stack) { |
|||
logger[level](namespace, `Stack: ${errorDetail.stack}`) |
|||
} |
|||
|
|||
// 如果有额外信息,单独记录
|
|||
if (errorDetail.additionalInfo) { |
|||
try { |
|||
const additionalInfoStr = JSON.stringify(errorDetail.additionalInfo, null, 2) |
|||
logger[level](namespace, `Additional Info: ${additionalInfoStr}`) |
|||
} catch (e) { |
|||
logger[level](namespace, "Additional Info: [Unserializable]") |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 空的安装全局错误处理器方法 |
|||
* 实际的全局错误处理由renderer-error.ts负责 |
|||
*/ |
|||
const installGlobalHandlers = () => { |
|||
// 不再在preload层安装全局错误处理器
|
|||
// 仅记录日志表明该方法被调用
|
|||
logger.info("[ErrorHandler] Global error handlers should be installed in renderer process") |
|||
} |
|||
|
|||
return { |
|||
captureError: handleError, |
|||
setOptions: (newOptions: Partial<ErrorHandlerOptions>) => { |
|||
options = { ...options, ...newOptions } |
|||
// 同步选项到主进程
|
|||
ipcRenderer.send("logger:errorOptions", options) |
|||
}, |
|||
getOptions: () => ({ ...options }), |
|||
installGlobalHandlers, |
|||
} |
|||
} |
|||
|
|||
const errorHandler = createRendererErrorHandler() |
|||
|
|||
// 暴露错误处理器到渲染进程全局
|
|||
contextBridge.exposeInMainWorld("preloadErrorHandler", errorHandler) |
|||
|
|||
// 导出类型定义,方便在渲染进程中使用
|
|||
export type { IRendererErrorHandler, ErrorDetail, ErrorHandlerOptions } |
@ -0,0 +1,153 @@ |
|||
import { contextBridge, ipcRenderer } from "electron" |
|||
import { LogLevel } from "./common" |
|||
|
|||
/** |
|||
* 渲染进程日志接口 |
|||
*/ |
|||
interface IRendererLogger { |
|||
trace(namespace: string, ...messages: any[]): void |
|||
debug(namespace: string, ...messages: any[]): void |
|||
info(namespace: string, ...messages: any[]): void |
|||
warn(namespace: string, ...messages: any[]): void |
|||
error(namespace: string, ...messages: any[]): void |
|||
fatal(namespace: string, ...messages: any[]): void |
|||
setLevel(level: LogLevel): void |
|||
createNamespace(namespace: string): INamespacedLogger |
|||
} |
|||
|
|||
/** |
|||
* 命名空间作用域日志接口 |
|||
*/ |
|||
interface INamespacedLogger { |
|||
trace(...messages: any[]): void |
|||
debug(...messages: any[]): void |
|||
info(...messages: any[]): void |
|||
warn(...messages: any[]): void |
|||
error(...messages: any[]): void |
|||
fatal(...messages: any[]): void |
|||
setLevel(level: LogLevel): void |
|||
} |
|||
|
|||
// 日志级别名称映射
|
|||
const LogLevelName: Record<LogLevel, string> = { |
|||
[LogLevel.TRACE]: "TRACE", |
|||
[LogLevel.DEBUG]: "DEBUG", |
|||
[LogLevel.INFO]: "INFO", |
|||
[LogLevel.WARN]: "WARN", |
|||
[LogLevel.ERROR]: "ERROR", |
|||
[LogLevel.FATAL]: "FATAL", |
|||
[LogLevel.OFF]: "OFF", |
|||
} |
|||
|
|||
// 日志颜色映射(控制台输出用)
|
|||
const LogLevelColor: Record<LogLevel, string> = { |
|||
[LogLevel.TRACE]: "\x1b[90m", // 灰色
|
|||
[LogLevel.DEBUG]: "\x1b[36m", // 青色
|
|||
[LogLevel.INFO]: "\x1b[32m", // 绿色
|
|||
[LogLevel.WARN]: "\x1b[33m", // 黄色
|
|||
[LogLevel.ERROR]: "\x1b[31m", // 红色
|
|||
[LogLevel.FATAL]: "\x1b[35m", // 紫色
|
|||
[LogLevel.OFF]: "", // 无色
|
|||
} |
|||
|
|||
// 重置颜色的ANSI代码
|
|||
const RESET_COLOR = "\x1b[0m" |
|||
|
|||
/** |
|||
* 创建渲染进程日志对象 |
|||
*/ |
|||
const createRendererLogger = (): IRendererLogger => { |
|||
// 当前日志级别
|
|||
let currentLevel: LogLevel = LogLevel.INFO |
|||
|
|||
// 格式化消息
|
|||
const formatMessages = (messages: any[]): string => { |
|||
return messages |
|||
.map(msg => { |
|||
if (typeof msg === "object") { |
|||
try { |
|||
return JSON.stringify(msg) |
|||
} catch (e) { |
|||
return String(msg) |
|||
} |
|||
} |
|||
return String(msg) |
|||
}) |
|||
.join(" ") |
|||
} |
|||
|
|||
// 本地打印日志
|
|||
const printLog = (level: LogLevel, namespace: string, ...messages: any[]): void => { |
|||
// 检查日志级别
|
|||
if (level < currentLevel || level === LogLevel.OFF) return |
|||
|
|||
const timestamp = new Date().toISOString() |
|||
const levelName = LogLevelName[level] |
|||
const prefix = `[${timestamp}] [${namespace}] [${levelName}]` |
|||
const message = formatMessages(messages) |
|||
|
|||
// 输出到控制台
|
|||
const color = LogLevelColor[level] |
|||
console.log(`${color}${prefix} ${message}${RESET_COLOR}`) |
|||
} |
|||
|
|||
// 通过IPC发送日志到主进程
|
|||
const sendLog = (level: LogLevel, namespace: string, ...messages: any[]) => { |
|||
// 本地打印
|
|||
printLog(level, namespace, ...messages) |
|||
|
|||
// 发送到主进程
|
|||
ipcRenderer.send("logger:log", level, namespace, ...messages) |
|||
} |
|||
|
|||
return { |
|||
trace(namespace: string, ...messages: any[]): void { |
|||
sendLog(LogLevel.TRACE, namespace, ...messages) |
|||
}, |
|||
debug(namespace: string, ...messages: any[]): void { |
|||
sendLog(LogLevel.DEBUG, namespace, ...messages) |
|||
}, |
|||
info(namespace: string, ...messages: any[]): void { |
|||
sendLog(LogLevel.INFO, namespace, ...messages) |
|||
}, |
|||
warn(namespace: string, ...messages: any[]): void { |
|||
sendLog(LogLevel.WARN, namespace, ...messages) |
|||
}, |
|||
error(namespace: string, ...messages: any[]): void { |
|||
sendLog(LogLevel.ERROR, namespace, ...messages) |
|||
}, |
|||
fatal(namespace: string, ...messages: any[]): void { |
|||
sendLog(LogLevel.FATAL, namespace, ...messages) |
|||
}, |
|||
setLevel(level: LogLevel): void { |
|||
// 更新本地日志级别
|
|||
currentLevel = level |
|||
// 设置日志级别(可选,如果需要在渲染进程中动态调整日志级别)
|
|||
ipcRenderer.send("logger:setLevel", level) |
|||
}, |
|||
createNamespace(namespace: string): INamespacedLogger { |
|||
return { |
|||
trace: (...messages: any[]) => sendLog(LogLevel.TRACE, namespace, ...messages), |
|||
debug: (...messages: any[]) => sendLog(LogLevel.DEBUG, namespace, ...messages), |
|||
info: (...messages: any[]) => sendLog(LogLevel.INFO, namespace, ...messages), |
|||
warn: (...messages: any[]) => sendLog(LogLevel.WARN, namespace, ...messages), |
|||
error: (...messages: any[]) => sendLog(LogLevel.ERROR, namespace, ...messages), |
|||
fatal: (...messages: any[]) => sendLog(LogLevel.FATAL, namespace, ...messages), |
|||
setLevel: (level: LogLevel) => { |
|||
currentLevel = level |
|||
ipcRenderer.send("logger:setLevel", level) |
|||
}, |
|||
} |
|||
}, |
|||
} |
|||
} |
|||
|
|||
const logger = createRendererLogger() |
|||
|
|||
// 暴露logger对象到渲染进程全局
|
|||
contextBridge.exposeInMainWorld("logger", logger) |
|||
|
|||
export { logger } |
|||
export default logger |
|||
// 导出类型定义,方便在渲染进程中使用
|
|||
export type { IRendererLogger } |
@ -0,0 +1,243 @@ |
|||
import { LogLevel } from "./common" |
|||
|
|||
/** |
|||
* 错误详情接口 |
|||
*/ |
|||
interface ErrorDetail { |
|||
message: string |
|||
stack?: string |
|||
componentInfo?: string |
|||
additionalInfo?: Record<string, any> |
|||
timestamp: string |
|||
type: string |
|||
} |
|||
|
|||
/** |
|||
* 错误处理配置 |
|||
*/ |
|||
interface ErrorHandlerOptions { |
|||
namespace?: string |
|||
level?: LogLevel |
|||
includeStack?: boolean |
|||
includeComponentInfo?: boolean |
|||
} |
|||
|
|||
/** |
|||
* 渲染进程错误处理接口 |
|||
*/ |
|||
export interface IRendererErrorHandler { |
|||
/** |
|||
* 捕获错误 |
|||
*/ |
|||
captureError(error: any, componentInfo?: string, additionalInfo?: Record<string, any>): void |
|||
|
|||
/** |
|||
* 设置错误处理选项 |
|||
*/ |
|||
setOptions(options: Partial<ErrorHandlerOptions>): void |
|||
|
|||
/** |
|||
* 获取当前选项 |
|||
*/ |
|||
getOptions(): ErrorHandlerOptions |
|||
|
|||
/** |
|||
* 安装全局错误处理器 |
|||
*/ |
|||
installGlobalHandlers(): void |
|||
} |
|||
|
|||
/** |
|||
* 默认错误处理配置 |
|||
*/ |
|||
const DEFAULT_OPTIONS: ErrorHandlerOptions = { |
|||
namespace: "error", |
|||
level: LogLevel.ERROR, |
|||
includeStack: true, |
|||
includeComponentInfo: true, |
|||
} |
|||
|
|||
/** |
|||
* 格式化错误信息 |
|||
*/ |
|||
const formatError = (error: any, options: ErrorHandlerOptions): ErrorDetail => { |
|||
// 基本错误信息
|
|||
const errorDetail: ErrorDetail = { |
|||
message: "", |
|||
timestamp: new Date().toISOString(), |
|||
type: "Unknown", |
|||
} |
|||
console.log(error) |
|||
|
|||
// 处理不同类型的错误
|
|||
if (error instanceof Error) { |
|||
errorDetail.message = error.message |
|||
errorDetail.type = error.name || error.constructor.name |
|||
if (options.includeStack) { |
|||
errorDetail.stack = error.stack |
|||
} |
|||
} else if (typeof error === "string") { |
|||
errorDetail.message = error |
|||
errorDetail.type = "String" |
|||
} else if (error === null) { |
|||
errorDetail.message = "Null error received" |
|||
errorDetail.type = "Null" |
|||
} else if (error === undefined) { |
|||
errorDetail.message = "Undefined error received" |
|||
errorDetail.type = "Undefined" |
|||
} else if (typeof error === "object") { |
|||
try { |
|||
errorDetail.message = error.message || JSON.stringify(error) |
|||
errorDetail.type = "Object" |
|||
errorDetail.additionalInfo = { ...error } |
|||
} catch (e) { |
|||
errorDetail.message = "Unserializable error object" |
|||
errorDetail.type = "Unserializable" |
|||
} |
|||
} else { |
|||
try { |
|||
errorDetail.message = String(error) |
|||
errorDetail.type = typeof error |
|||
} catch (e) { |
|||
errorDetail.message = "Error converting to string" |
|||
errorDetail.type = "Unknown" |
|||
} |
|||
} |
|||
|
|||
return errorDetail |
|||
} |
|||
|
|||
// @ts-ignore
|
|||
const preloadErrorHandler = window.preloadErrorHandler |
|||
|
|||
/** |
|||
* 创建渲染进程错误处理器 |
|||
*/ |
|||
export const createRendererErrorHandler = (): IRendererErrorHandler => { |
|||
// 当前错误处理选项
|
|||
let options: ErrorHandlerOptions = { ...DEFAULT_OPTIONS } |
|||
|
|||
/** |
|||
* 处理错误并序列化 |
|||
*/ |
|||
const processError = (error: any, componentInfo?: string, additionalInfo?: Record<string, any>): ErrorDetail => { |
|||
const errorDetail = formatError(error, options) |
|||
|
|||
// 添加组件信息
|
|||
if (options.includeComponentInfo && componentInfo) { |
|||
errorDetail.componentInfo = componentInfo |
|||
} |
|||
|
|||
// 添加额外信息
|
|||
if (additionalInfo) { |
|||
errorDetail.additionalInfo = { |
|||
...errorDetail.additionalInfo, |
|||
...additionalInfo, |
|||
} |
|||
} |
|||
|
|||
return errorDetail |
|||
} |
|||
|
|||
/** |
|||
* 发送错误到preload层 |
|||
*/ |
|||
const sendError = (error: any, componentInfo?: string, additionalInfo?: Record<string, any>) => { |
|||
// 处理并序列化错误
|
|||
const errorDetail = processError(error, componentInfo, additionalInfo) |
|||
|
|||
// 调用window.errorHandler.captureError发送错误
|
|||
// 这里假设preload层已经暴露了errorHandler对象
|
|||
if (preloadErrorHandler && typeof preloadErrorHandler.captureError === "function") { |
|||
preloadErrorHandler.captureError(errorDetail) |
|||
} else { |
|||
// 如果errorHandler不可用,则降级到控制台输出
|
|||
console.error("[ErrorHandler]", errorDetail) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 安装全局错误处理器 |
|||
*/ |
|||
const installGlobalHandlers = () => { |
|||
// 捕获未处理的异常
|
|||
window.addEventListener("error", event => { |
|||
event.preventDefault() |
|||
sendError(event.error || event.message, "window.onerror", { |
|||
filename: event.filename, |
|||
lineno: event.lineno, |
|||
colno: event.colno, |
|||
}) |
|||
return true |
|||
}) |
|||
|
|||
// 捕获未处理的Promise拒绝
|
|||
window.addEventListener("unhandledrejection", event => { |
|||
event.preventDefault() |
|||
sendError(event.reason, "unhandledrejection", { |
|||
promise: "[Promise]", // 不能直接序列化Promise对象
|
|||
}) |
|||
return true |
|||
}) |
|||
|
|||
// 捕获资源加载错误
|
|||
document.addEventListener( |
|||
"error", |
|||
event => { |
|||
// 只处理资源加载错误
|
|||
if (event.target && (event.target as HTMLElement).tagName) { |
|||
const target = event.target as HTMLElement |
|||
sendError(`Resource load failed: ${(target as any).src || (target as any).href}`, "resource.error", { |
|||
tagName: target.tagName, |
|||
src: (target as any).src, |
|||
href: (target as any).href, |
|||
}) |
|||
} |
|||
}, |
|||
true, |
|||
) // 使用捕获阶段
|
|||
|
|||
console.info("[ErrorHandler] Global error handlers installed") |
|||
} |
|||
|
|||
return { |
|||
captureError: sendError, |
|||
setOptions: (newOptions: Partial<ErrorHandlerOptions>) => { |
|||
options = { ...options, ...newOptions } |
|||
// 同步选项到preload层
|
|||
if (preloadErrorHandler && typeof preloadErrorHandler.setOptions === "function") { |
|||
preloadErrorHandler.setOptions(options) |
|||
} |
|||
}, |
|||
getOptions: () => ({ ...options }), |
|||
installGlobalHandlers, |
|||
} |
|||
} |
|||
|
|||
// 导出类型定义,方便在渲染进程中使用
|
|||
export type { ErrorDetail, ErrorHandlerOptions } |
|||
|
|||
// 创建渲染进程错误处理器
|
|||
const errorHandler = createRendererErrorHandler() |
|||
|
|||
// 安装全局错误处理器
|
|||
errorHandler.installGlobalHandlers() |
|||
|
|||
window.errorHandler = errorHandler |
|||
|
|||
/** |
|||
* 使用示例: |
|||
* |
|||
* // 捕获特定错误
|
|||
* try { |
|||
* // 可能出错的代码
|
|||
* } catch (error) { |
|||
* errorHandler.captureError(error, 'ComponentName', { additionalInfo: 'value' }) |
|||
* } |
|||
* |
|||
* // 设置错误处理选项
|
|||
* errorHandler.setOptions({ |
|||
* namespace: 'custom-error', |
|||
* includeComponentInfo: true |
|||
* }) |
|||
*/ |
@ -0,0 +1,231 @@ |
|||
import fs from "fs-extra" |
|||
import { app } from "electron" |
|||
import path from "path" |
|||
import { cloneDeep } from "lodash" |
|||
import Config from "config" |
|||
import type { IDefaultConfig } from "config" |
|||
import _logger from "logger/main" |
|||
|
|||
const logger = _logger.createNamespace("setting") |
|||
|
|||
type IConfig = IDefaultConfig |
|||
|
|||
type IOnFunc = (n: IConfig, c: IConfig, keys?: (keyof IConfig)[]) => void |
|||
type IT = (keyof IConfig)[] | keyof IConfig | "_" |
|||
|
|||
let storagePath = path.join(app.getPath("documents"), Config.app_title) |
|||
const storagePathDev = path.join(app.getPath("documents"), Config.app_title + "-dev") |
|||
|
|||
if (process.env.NODE_ENV === "development") { |
|||
storagePath = storagePathDev |
|||
} |
|||
|
|||
const _tempConfig = cloneDeep(Config.default_config as IConfig) |
|||
Object.keys(_tempConfig).forEach(key => { |
|||
if (typeof _tempConfig[key] === "string" && _tempConfig[key].includes("$storagePath$")) { |
|||
_tempConfig[key] = _tempConfig[key].replace(/\$storagePath\$/g, storagePath) |
|||
if (_tempConfig[key] && path.isAbsolute(_tempConfig[key])) { |
|||
_tempConfig[key] = path.normalize(_tempConfig[key]) |
|||
} |
|||
} |
|||
}) |
|||
|
|||
function isPath(str) { |
|||
// 使用正则表达式检查字符串是否以斜杠或盘符开头
|
|||
return /^(?:\/|[a-zA-Z]:\\)/.test(str) |
|||
} |
|||
|
|||
function init(config: IConfig) { |
|||
// 在配置初始化后执行
|
|||
Object.keys(config).forEach(key => { |
|||
if (config[key] && isPath(config[key]) && path.isAbsolute(config[key])) { |
|||
fs.ensureDirSync(config[key]) |
|||
} |
|||
}) |
|||
// 在配置初始化后执行
|
|||
// fs.ensureDirSync(config["snippet.storagePath"])
|
|||
// fs.ensureDirSync(config["bookmark.storagePath"])
|
|||
} |
|||
|
|||
// 判断是否是空文件夹
|
|||
function isEmptyDir(fPath: string) { |
|||
const pa = fs.readdirSync(fPath) |
|||
if (pa.length === 0) { |
|||
return true |
|||
} else { |
|||
return false |
|||
} |
|||
} |
|||
|
|||
class SettingClass { |
|||
constructor() { |
|||
logger.debug(`Setting inited`) |
|||
this.init() |
|||
} |
|||
|
|||
#cb: [IT, IOnFunc][] = [] |
|||
|
|||
onChange(fn: IOnFunc, that?: any) |
|||
onChange(key: IT, fn: IOnFunc, that?: any) |
|||
onChange(fnOrType: IT | IOnFunc, fnOrThat: IOnFunc | any = null, that: any = null) { |
|||
if (typeof fnOrType === "function") { |
|||
this.#cb.push(["_", fnOrType.bind(fnOrThat)]) |
|||
} else { |
|||
this.#cb.push([fnOrType, fnOrThat.bind(that)]) |
|||
} |
|||
} |
|||
|
|||
#runCB(n: IConfig, c: IConfig, keys: (keyof IConfig)[]) { |
|||
for (let i = 0; i < this.#cb.length; i++) { |
|||
const temp = this.#cb[i] |
|||
const k = temp[0] |
|||
const fn = temp[1] |
|||
if (k === "_") { |
|||
fn(n, c, keys) |
|||
} |
|||
if (typeof k === "string" && keys.includes(k as keyof IConfig)) { |
|||
fn(n, c) |
|||
} |
|||
if (Array.isArray(k) && k.filter(v => keys.indexOf(v) !== -1).length) { |
|||
fn(n, c) |
|||
} |
|||
} |
|||
} |
|||
|
|||
#pathFile: string = |
|||
process.env.NODE_ENV === "development" |
|||
? path.resolve(app.getPath("userData"), "./config_path-dev") |
|||
: path.resolve(app.getPath("userData"), "./config_path") |
|||
#config: IConfig = cloneDeep(_tempConfig) |
|||
#configPath(storagePath?: string): string { |
|||
return path.join(storagePath || this.#config.storagePath, "./config.json") |
|||
} |
|||
/** |
|||
* 读取配置文件变量同步 |
|||
* @param confingPath 配置文件路径 |
|||
*/ |
|||
#syncVar(confingPath?: string) { |
|||
const configFile = this.#configPath(confingPath) |
|||
if (!fs.pathExistsSync(configFile)) { |
|||
fs.ensureFileSync(configFile) |
|||
fs.writeJSONSync(configFile, {}) |
|||
} |
|||
const config = fs.readJSONSync(configFile) as IConfig |
|||
confingPath && (config.storagePath = confingPath) |
|||
// 优先取本地的值
|
|||
for (const key in config) { |
|||
// if (Object.prototype.hasOwnProperty.call(this.#config, key)) {
|
|||
// this.#config[key] = config[key] || this.#config[key]
|
|||
// }
|
|||
// 删除配置时本地的配置不会改变,想一下哪种方式更好
|
|||
this.#config[key] = config[key] ?? this.#config[key] |
|||
} |
|||
} |
|||
init() { |
|||
logger.debug(`位置:${this.#pathFile}`) |
|||
|
|||
if (fs.pathExistsSync(this.#pathFile)) { |
|||
const confingPath = fs.readFileSync(this.#pathFile, { encoding: "utf8" }) |
|||
if (confingPath && fs.pathExistsSync(this.#configPath(confingPath))) { |
|||
this.#syncVar(confingPath) |
|||
// 防止增加了配置本地却没变的情况
|
|||
this.#sync(confingPath) |
|||
} else { |
|||
this.#syncVar(confingPath) |
|||
this.#sync(confingPath) |
|||
} |
|||
} else { |
|||
this.#syncVar() |
|||
this.#sync() |
|||
} |
|||
init.call(this, this.#config) |
|||
} |
|||
config() { |
|||
return this.#config |
|||
} |
|||
#sync(c?: string) { |
|||
const config = cloneDeep(this.#config) |
|||
delete config.storagePath |
|||
const p = this.#configPath(c) |
|||
fs.ensureFileSync(p) |
|||
fs.writeJSONSync(this.#configPath(c), config) |
|||
} |
|||
#change(p: string) { |
|||
const storagePath = this.#config.storagePath |
|||
if (fs.existsSync(storagePath) && !fs.existsSync(p)) { |
|||
fs.moveSync(storagePath, p) |
|||
} |
|||
if (fs.existsSync(p) && fs.existsSync(storagePath) && isEmptyDir(p)) { |
|||
fs.moveSync(storagePath, p, { overwrite: true }) |
|||
} |
|||
fs.writeFileSync(this.#pathFile, p, { encoding: "utf8" }) |
|||
} |
|||
reset(key: keyof IConfig) { |
|||
this.set(key, cloneDeep(_tempConfig[key])) |
|||
} |
|||
set(key: keyof IConfig | Partial<IConfig>, value?: any) { |
|||
const oldMainConfig = Object.assign({}, this.#config) |
|||
let isChange = false |
|||
const changeKeys: (keyof IConfig)[] = [] |
|||
const canChangeStorage = (targetPath: string) => { |
|||
if (fs.existsSync(oldMainConfig.storagePath) && fs.existsSync(targetPath) && !isEmptyDir(targetPath)) { |
|||
if (fs.existsSync(path.join(targetPath, "./config.json"))) { |
|||
return true |
|||
} |
|||
return false |
|||
} |
|||
return true |
|||
} |
|||
if (typeof key === "string") { |
|||
if (value != undefined && value !== this.#config[key]) { |
|||
if (key === "storagePath") { |
|||
if (!canChangeStorage(value)) { |
|||
throw "无法改变存储地址" |
|||
return |
|||
} |
|||
this.#change(value) |
|||
changeKeys.push("storagePath") |
|||
this.#config["storagePath"] = value |
|||
} else { |
|||
changeKeys.push(key) |
|||
this.#config[key as string] = value |
|||
} |
|||
isChange = true |
|||
} |
|||
} else { |
|||
if (key["storagePath"] !== undefined && key["storagePath"] !== this.#config["storagePath"]) { |
|||
if (!canChangeStorage(key["storagePath"])) { |
|||
throw "无法改变存储地址" |
|||
return |
|||
} |
|||
this.#change(key["storagePath"]) |
|||
this.#config["storagePath"] = key["storagePath"] |
|||
changeKeys.push("storagePath") |
|||
isChange = true |
|||
} |
|||
for (const _ in key) { |
|||
if (Object.prototype.hasOwnProperty.call(key, _)) { |
|||
const v = key[_] |
|||
if (v != undefined && _ !== "storagePath" && v !== this.#config[_]) { |
|||
this.#config[_] = v |
|||
changeKeys.push(_ as keyof IConfig) |
|||
isChange = true |
|||
} |
|||
} |
|||
} |
|||
} |
|||
if (isChange) { |
|||
this.#sync() |
|||
this.#runCB(this.#config, oldMainConfig, changeKeys) |
|||
} |
|||
} |
|||
values<T extends keyof IConfig>(key: T): IConfig[T] { |
|||
return this.#config[key] |
|||
} |
|||
} |
|||
|
|||
const Setting = new SettingClass() |
|||
|
|||
export default Setting |
|||
export { Setting } |
|||
export type { IConfig, IOnFunc } |
@ -0,0 +1,6 @@ |
|||
import { buildEmitter } from "base/event/main" |
|||
import type { IOnFunc } from "setting/main" |
|||
|
|||
export const emitter = buildEmitter<{ |
|||
update: IOnFunc |
|||
}>() |
@ -0,0 +1,7 @@ |
|||
{ |
|||
"name": "setting", |
|||
"version": "1.0.0", |
|||
"keywords": [], |
|||
"author": "", |
|||
"license": "ISC" |
|||
} |
File diff suppressed because it is too large
@ -1,2 +1,2 @@ |
|||
packages: |
|||
- "packages/*" |
|||
- "packages/*" |
|||
|
@ -1,11 +1,12 @@ |
|||
<!DOCTYPE html> |
|||
<!doctype html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|||
<head> |
|||
<meta charset="UTF-8" /> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
|||
<title>Document</title> |
|||
</head> |
|||
<body> |
|||
前往 <a href="https://baidu.com" target="_blank">百度</a> |
|||
</body> |
|||
</head> |
|||
<body> |
|||
前往 |
|||
<a href="https://baidu.com" target="_blank">百度</a> |
|||
</body> |
|||
</html> |
|||
|
@ -0,0 +1,34 @@ |
|||
import { Container, ContainerModule } from "inversify" |
|||
|
|||
/** |
|||
* 自动加载所有命令模块 |
|||
*/ |
|||
const commandModules = import.meta.glob("./event/**/main/command.{ts,js}", { eager: true }) |
|||
|
|||
const modules = new ContainerModule(bind => { |
|||
// 自动绑定所有命令类
|
|||
Object.values(commandModules).forEach(module => { |
|||
// 由于 module 类型为 unknown,先进行类型断言为包含 default 属性的对象
|
|||
const CommandClass = (module as { default: any }).default |
|||
if (CommandClass) { |
|||
const className = CommandClass.name.replace("Command", "") |
|||
if (CommandClass["init"]) { |
|||
CommandClass["init"]() |
|||
} |
|||
bind(className + "Command") |
|||
.to(CommandClass) |
|||
.inSingletonScope() |
|||
} |
|||
}) |
|||
}) |
|||
|
|||
/** |
|||
* 销毁所有命令绑定 |
|||
* @param ioc - Inversify 容器实例 |
|||
*/ |
|||
async function destroyAllCommand(ioc: Container) { |
|||
await ioc.unloadAsync(modules) |
|||
} |
|||
|
|||
export { modules, destroyAllCommand } |
|||
export default modules |
@ -0,0 +1,5 @@ |
|||
import { PlatForm } from "." |
|||
|
|||
export function usePlatForm() { |
|||
return PlatForm.getInstance<PlatForm>() |
|||
} |
@ -0,0 +1,58 @@ |
|||
import { ApiFactory } from "common/lib/abstract" |
|||
import { BaseSingleton } from "base" |
|||
import { LogLevel } from "packages/logger/common" |
|||
|
|||
class PlatForm extends BaseSingleton { |
|||
constructor() { |
|||
super() |
|||
} |
|||
|
|||
private get api() { |
|||
return ApiFactory.getApiClient() |
|||
} |
|||
|
|||
async logSetLevel(level: LogLevel) { |
|||
return this.api.call("PlatFormCommand.logSetLevel", level) |
|||
} |
|||
|
|||
async logGetLevel() { |
|||
return this.api.call("PlatFormCommand.logGetLevel") |
|||
} |
|||
|
|||
async showAbout() { |
|||
// return this.api.call("BasicService.showAbout")
|
|||
return this.api.call("PlatFormCommand.showAbout") |
|||
} |
|||
|
|||
async showSrd() { |
|||
// return this.api.call("BasicService.showAbout")
|
|||
return this.api.call("PlatFormCommand.showSrd") |
|||
} |
|||
|
|||
async getSrdCookie() { |
|||
// return this.api.call("BasicService.showAbout")
|
|||
return this.api.call("PlatFormCommand.getSrdCookie") |
|||
} |
|||
|
|||
async crash() { |
|||
return this.api.call("PlatFormCommand.crash") |
|||
} |
|||
|
|||
async isFullScreen() { |
|||
return this.api.call("PlatFormCommand.isFullscreen") |
|||
} |
|||
|
|||
async toggleFullScreen() { |
|||
return this.api.call("PlatFormCommand.fullscreen") |
|||
} |
|||
|
|||
async reload() { |
|||
return this.api.call("PlatFormCommand.reload") |
|||
} |
|||
|
|||
async toggleDevTools() { |
|||
return this.api.call("PlatFormCommand.toggleDevTools") |
|||
} |
|||
} |
|||
|
|||
export { PlatForm } |
@ -0,0 +1,121 @@ |
|||
import { app, dialog, nativeTheme, TitleBarOverlayOptions } from "electron" |
|||
import { inject } from "inversify" |
|||
import errorHandler from "logger/main-error" |
|||
import Tabs from "main/modules/tabs" |
|||
import WindowManager from "main/modules/window-manager" |
|||
import { getFileUrl } from "main/utils" |
|||
import icon from "@res/icon.png?asset" |
|||
import setting from "setting/main" |
|||
import { LogLevel } from "logger/common" |
|||
|
|||
export default class PlatFormCommand { |
|||
constructor( |
|||
@inject(WindowManager) private _WindowManager: WindowManager, |
|||
@inject(Tabs) private _Tabs: Tabs, |
|||
) {} |
|||
|
|||
setTheme(theme: typeof nativeTheme.themeSource) { |
|||
nativeTheme.themeSource = theme |
|||
} |
|||
|
|||
logSetLevel(level: LogLevel) { |
|||
return setting.set("debug", level) |
|||
} |
|||
|
|||
logGetLevel() { |
|||
return setting.values("debug") |
|||
} |
|||
|
|||
setTitlBar(options: TitleBarOverlayOptions) { |
|||
const mainWindow = this._WindowManager.getMainWindow() |
|||
if (mainWindow) { |
|||
mainWindow.setTitleBarOverlay(options) |
|||
} |
|||
} |
|||
|
|||
showAbout() { |
|||
this._WindowManager.createWindow("about", { |
|||
url: getFileUrl("about.html"), |
|||
overideWindowOpts: true, |
|||
confrimWindowClose: false, |
|||
type: "info", |
|||
windowOpts: { |
|||
width: 600, |
|||
height: 400, |
|||
minimizable: false, |
|||
darkTheme: true, |
|||
modal: true, |
|||
title: "关于我", |
|||
show: true, |
|||
resizable: false, |
|||
icon: icon, |
|||
webPreferences: { |
|||
devTools: false, |
|||
sandbox: false, |
|||
nodeIntegration: false, |
|||
contextIsolation: true, |
|||
}, |
|||
}, |
|||
}) |
|||
} |
|||
|
|||
toggleDevTools() { |
|||
const focusedWindow = this._WindowManager.getFocusWindow() |
|||
if (focusedWindow) { |
|||
// @ts-ignore ...
|
|||
focusedWindow.toggleDevTools() |
|||
} |
|||
} |
|||
fullscreen() { |
|||
const focusedWindow = this._WindowManager.getFocusWindow() |
|||
if (focusedWindow) { |
|||
const isFullScreen = focusedWindow!.isFullScreen() |
|||
focusedWindow!.setFullScreen(!isFullScreen) |
|||
} |
|||
} |
|||
|
|||
crash() { |
|||
errorHandler.captureError(new Error("手动触发的崩溃")) |
|||
process.crash() |
|||
} |
|||
|
|||
isFullscreen() { |
|||
const focusedWindow = this._WindowManager.getFocusWindow() |
|||
if (focusedWindow) { |
|||
return focusedWindow!.isFullScreen() |
|||
} |
|||
return false |
|||
} |
|||
|
|||
relunch() { |
|||
app.relaunch() |
|||
app.exit() |
|||
} |
|||
|
|||
reload() { |
|||
const focusedWindow = this._WindowManager.getFocusWindow() |
|||
// 重载之后, 刷新并关闭所有的次要窗体
|
|||
if (this._WindowManager.length() > 1 && focusedWindow && focusedWindow.$$opts!.name === this._WindowManager.mainInfo.name) { |
|||
const choice = dialog.showMessageBoxSync(focusedWindow, { |
|||
type: "question", |
|||
buttons: ["取消", "是的,继续", "不,算了"], |
|||
title: "警告", |
|||
defaultId: 2, |
|||
cancelId: 0, |
|||
message: "警告", |
|||
detail: "重载主窗口将关闭所有子窗口,是否继续", |
|||
}) |
|||
if (choice == 1) { |
|||
this._WindowManager.getWndows().forEach(win => { |
|||
if (win.$$opts!.name !== this._WindowManager.mainInfo.name) { |
|||
win.close() |
|||
} |
|||
}) |
|||
} else { |
|||
return |
|||
} |
|||
} |
|||
this._Tabs.closeAll() |
|||
focusedWindow!.reload() |
|||
} |
|||
} |
@ -0,0 +1,5 @@ |
|||
import { Snippet } from "." |
|||
|
|||
export function useSnippet() { |
|||
return Snippet.getInstance() |
|||
} |
@ -0,0 +1,18 @@ |
|||
import { BaseSingleton } from "base" |
|||
import { ApiFactory } from "common/lib/abstract" |
|||
|
|||
class Snippet extends BaseSingleton { |
|||
constructor() { |
|||
super() |
|||
} |
|||
|
|||
private get api() { |
|||
return ApiFactory.getApiClient() |
|||
} |
|||
|
|||
getTree = async () => { |
|||
return this.api.call("SnippetCommand.getTree") |
|||
} |
|||
} |
|||
|
|||
export { Snippet } |
@ -0,0 +1,44 @@ |
|||
import fs from "fs-extra" |
|||
import path from "path/posix" |
|||
import Setting from "setting/main" |
|||
|
|||
// 代码片段命令处理器
|
|||
// base/__snippet__.json 基础信息
|
|||
// 路径做为ID, 当前文件夹的信息
|
|||
|
|||
export default class SnippetCommand { |
|||
storagePath: string = Setting.values("snippet.storagePath") |
|||
|
|||
constructor() { |
|||
const handler = { |
|||
get: function (target, prop, receiver) { |
|||
if (!target["check"]()) { |
|||
throw new Error(`代码片段路径存在问题`) |
|||
} |
|||
const value = target[prop] |
|||
if (typeof value === "function") { |
|||
return (...args) => Reflect.apply(value, receiver, args) |
|||
} |
|||
return value |
|||
}, |
|||
} |
|||
return new Proxy(this, handler) |
|||
} |
|||
|
|||
async check() { |
|||
const stat = await fs.statSync(this.storagePath) |
|||
const inforFile = path.resolve(this.storagePath, "__snippet__.json") |
|||
if (stat.isDirectory() && stat.size == 0) { |
|||
await fs.writeJSON(inforFile, {}) |
|||
// 空文件夹, 初始化信息
|
|||
return true |
|||
} else { |
|||
const isExist = await fs.pathExists(inforFile) |
|||
return isExist |
|||
} |
|||
} |
|||
|
|||
getTree() { |
|||
return this.storagePath |
|||
} |
|||
} |
@ -0,0 +1,54 @@ |
|||
import { BaseSingleton } from "base" |
|||
|
|||
export class Tabs extends BaseSingleton { |
|||
constructor() { |
|||
super() |
|||
} |
|||
|
|||
private isListen: boolean = false |
|||
|
|||
private execUpdate = (...args) => { |
|||
this.#fnList.forEach(v => v(...args)) |
|||
} |
|||
|
|||
#fnList: ((...args) => void)[] = [] |
|||
listenUpdate(cb: (...args) => void) { |
|||
if (!this.isListen) { |
|||
api.on("main:TabsCommand.update", this.execUpdate) |
|||
this.isListen = true |
|||
} |
|||
this.#fnList.push(cb) |
|||
} |
|||
|
|||
unListenUpdate(fn: (...args) => void) { |
|||
this.#fnList = this.#fnList.filter(v => { |
|||
return v !== fn |
|||
}) |
|||
if (!this.#fnList.length) { |
|||
api.off("main:TabsCommand.update", this.execUpdate) |
|||
this.isListen = false |
|||
} |
|||
} |
|||
|
|||
bindPosition(data) { |
|||
api.call("TabsCommand.bindElement", data) |
|||
} |
|||
|
|||
closeAll() { |
|||
api.call("TabsCommand.closeAll") |
|||
} |
|||
|
|||
sync() { |
|||
api.call("TabsCommand.sync") |
|||
} |
|||
|
|||
unListenerAll() { |
|||
this.#fnList = [] |
|||
api.offAll("main:TabsCommand.update") |
|||
} |
|||
|
|||
async getAllTabs() { |
|||
const res = await api.call("TabsCommand.getAllTabs") |
|||
return res |
|||
} |
|||
} |
@ -0,0 +1,65 @@ |
|||
import { inject } from "inversify" |
|||
import Tabs from "main/modules/tabs" |
|||
import WindowManager from "main/modules/window-manager" |
|||
import { broadcast } from "main/utils" |
|||
|
|||
class TabsCommand { |
|||
constructor( |
|||
@inject(Tabs) private _Tabs: Tabs, |
|||
@inject(WindowManager) private _WindowManager: WindowManager, |
|||
) { |
|||
this._Tabs.events.on("update", this.listenerTabActive) |
|||
} |
|||
|
|||
listenerTabActive = () => { |
|||
broadcast("main:TabsCommand.update", this.getAllTabs()) |
|||
} |
|||
|
|||
bindElement(rect) { |
|||
this._Tabs.updateRect(rect) |
|||
} |
|||
|
|||
reload() { |
|||
this._WindowManager.getMainWindow()?.reload() |
|||
} |
|||
|
|||
sync() { |
|||
this.listenerTabActive() |
|||
if (!this.getAllTabs().length) { |
|||
this.add("about:blank") |
|||
} |
|||
} |
|||
|
|||
add(url) { |
|||
this._Tabs.add(url, true, this._WindowManager.getMainWindow()!) |
|||
} |
|||
|
|||
nagivate(index: number, url: string) { |
|||
this._Tabs.navigate(+index, url) |
|||
} |
|||
|
|||
closeAll() { |
|||
this._Tabs.closeAll() |
|||
} |
|||
|
|||
setActive(index) { |
|||
this._Tabs.changeActive(index) |
|||
} |
|||
|
|||
closeTab(e) { |
|||
this._Tabs.remove(e.body.active) |
|||
} |
|||
|
|||
getAllTabs() { |
|||
return this._Tabs._tabs.map(v => ({ |
|||
url: v.url, |
|||
showUrl: v.showUrl, |
|||
title: v.title, |
|||
favicons: v.favicons, |
|||
isActive: v.isActive, |
|||
})) |
|||
} |
|||
} |
|||
|
|||
export { TabsCommand } |
|||
export default TabsCommand |
@ -0,0 +1,15 @@ |
|||
import { EventEnum } from "helper/updater/common" |
|||
|
|||
const curProgress = ref(0) |
|||
|
|||
api.on(EventEnum.UPDATE_PROGRESS, ({ percent, now, all }) => { |
|||
curProgress.value = percent |
|||
}) |
|||
|
|||
function useUpdate() { |
|||
return { |
|||
curProgress, |
|||
} |
|||
} |
|||
|
|||
export { useUpdate } |
@ -0,0 +1,15 @@ |
|||
import Updater from "helper/updater/main" |
|||
import _logger from "logger/main" |
|||
|
|||
const logger = _logger.createNamespace("UpdaterCommand") |
|||
|
|||
export default class UpdaterCommand { |
|||
static init() { |
|||
// 命令初始化
|
|||
logger.debug("UpdaterCommand init") |
|||
} |
|||
|
|||
async triggerHotUpdate() { |
|||
Updater.triggerHotUpdate() |
|||
} |
|||
} |
@ -0,0 +1,53 @@ |
|||
import { ElectronApiClient } from "common/lib/electron" |
|||
import { BrowserApiClient } from "common/lib/browser" |
|||
|
|||
// 定义抽象 API 接口
|
|||
export interface IApiClient { |
|||
call<T = any>(command: string, ...args: any[]): Promise<T> |
|||
on<K extends string>(channel: K, callback: (...args: any[]) => void): void |
|||
off<K extends string>(channel: K, callback: (...args: any[]) => void): void |
|||
offAll<K extends string>(channel: K): void |
|||
} |
|||
|
|||
class NullApiClient implements IApiClient { |
|||
async call<T = any>(command: string, ...args: any[]): Promise<T> { |
|||
args |
|||
console.warn(`API call to ${command} failed: API client not initialized`) |
|||
return undefined as any |
|||
} |
|||
|
|||
on<K extends string>(channel: K, callback: (...args: any[]) => void): void { |
|||
callback |
|||
console.warn(`Failed to register listener for ${channel}: API client not initialized`) |
|||
} |
|||
|
|||
off<K extends string>(channel: K, callback: (...args: any[]) => void): void { |
|||
callback |
|||
console.warn(`Failed to unregister listener for ${channel}: API client not initialized`) |
|||
} |
|||
|
|||
offAll<K extends string>(channel: K): void { |
|||
console.warn(`Failed to unregister all listeners for ${channel}: API client not initialized`) |
|||
} |
|||
} |
|||
|
|||
// 创建 API 工厂
|
|||
export class ApiFactory { |
|||
private static instance: IApiClient = new NullApiClient() // 默认使用空实现
|
|||
|
|||
static setApiClient(client: IApiClient) { |
|||
this.instance = client |
|||
} |
|||
|
|||
static getApiClient(): IApiClient { |
|||
if (this.instance instanceof NullApiClient) { |
|||
// 根据环境选择合适的 API 客户端
|
|||
if (window.api && window.electron) { |
|||
this.instance = new ElectronApiClient() |
|||
} else { |
|||
this.instance = new BrowserApiClient() |
|||
} |
|||
} |
|||
return this.instance |
|||
} |
|||
} |
@ -0,0 +1,29 @@ |
|||
import { IApiClient } from "./abstract" |
|||
|
|||
export class BrowserApiClient implements IApiClient { |
|||
call<T = any>(command: string, ...args: any[]): Promise<T> { |
|||
// 浏览器特定实现,可能使用 fetch 或其他方式
|
|||
const [service, method] = command.split(".") |
|||
return fetch(`/api/${service}/${method}`, { |
|||
method: "POST", |
|||
body: JSON.stringify(args), |
|||
headers: { "Content-Type": "application/json" }, |
|||
}).then(res => res.json()) |
|||
} |
|||
|
|||
// 实现其他方法...
|
|||
on<K extends string>(channel: K, callback: (...args: any[]) => void): void { |
|||
// 浏览器中可能使用 WebSocket 或其他方式
|
|||
console.log("不支持 on 方法", channel, callback) |
|||
} |
|||
|
|||
off<K extends string>(channel: K, callback: (...args: any[]) => void): void { |
|||
// 相应的解绑实现
|
|||
console.log("不支持 on 方法", channel, callback) |
|||
} |
|||
|
|||
offAll<K extends string>(channel: K): void { |
|||
// 相应的全部解绑实现
|
|||
console.log("不支持 on 方法", channel) |
|||
} |
|||
} |
@ -0,0 +1,20 @@ |
|||
import { IApiClient } from "./abstract" |
|||
|
|||
export class ElectronApiClient implements IApiClient { |
|||
call<T = any>(command: string, ...args: any[]): Promise<T> { |
|||
// Electron 特定实现
|
|||
return window.api.call(command, ...args) |
|||
} |
|||
|
|||
on<K extends string>(channel: K, callback: (...args: any[]) => void): void { |
|||
window.api.on(channel, callback) |
|||
} |
|||
|
|||
off<K extends string>(channel: K, callback: (...args: any[]) => void): void { |
|||
window.api.off(channel, callback) |
|||
} |
|||
|
|||
offAll<K extends string>(channel: K): void { |
|||
window.api.offAll(channel) |
|||
} |
|||
} |
@ -0,0 +1,7 @@ |
|||
## event |
|||
|
|||
通用事件处理模块 |
|||
|
|||
- main/**/* 处理主进程的模块 |
|||
- main/command.ts 会通过ioc收集,进入依赖管理中 |
|||
- 其他 处理渲染进程的模块 |
@ -0,0 +1,130 @@ |
|||
class Node<E> { |
|||
static readonly Undefined = new Node<any>(undefined) |
|||
|
|||
element: E |
|||
next: Node<E> |
|||
prev: Node<E> |
|||
|
|||
constructor(element: E) { |
|||
this.element = element |
|||
this.next = Node.Undefined |
|||
this.prev = Node.Undefined |
|||
} |
|||
} |
|||
|
|||
export class LinkedList<E> { |
|||
private _first: Node<E> = Node.Undefined |
|||
private _last: Node<E> = Node.Undefined |
|||
private _size: number = 0 |
|||
|
|||
get size(): number { |
|||
return this._size |
|||
} |
|||
|
|||
isEmpty(): boolean { |
|||
return this._first === Node.Undefined |
|||
} |
|||
|
|||
clear(): void { |
|||
let node = this._first |
|||
while (node !== Node.Undefined) { |
|||
const next = node.next |
|||
node.prev = Node.Undefined |
|||
node.next = Node.Undefined |
|||
node = next |
|||
} |
|||
|
|||
this._first = Node.Undefined |
|||
this._last = Node.Undefined |
|||
this._size = 0 |
|||
} |
|||
|
|||
unshift(element: E): () => void { |
|||
return this._insert(element, false) |
|||
} |
|||
|
|||
push(element: E): () => void { |
|||
return this._insert(element, true) |
|||
} |
|||
|
|||
private _insert(element: E, atTheEnd: boolean): () => void { |
|||
const newNode = new Node(element) |
|||
if (this._first === Node.Undefined) { |
|||
this._first = newNode |
|||
this._last = newNode |
|||
} else if (atTheEnd) { |
|||
// push
|
|||
const oldLast = this._last |
|||
this._last = newNode |
|||
newNode.prev = oldLast |
|||
oldLast.next = newNode |
|||
} else { |
|||
// unshift
|
|||
const oldFirst = this._first |
|||
this._first = newNode |
|||
newNode.next = oldFirst |
|||
oldFirst.prev = newNode |
|||
} |
|||
this._size += 1 |
|||
|
|||
let didRemove = false |
|||
return () => { |
|||
if (!didRemove) { |
|||
didRemove = true |
|||
this._remove(newNode) |
|||
} |
|||
} |
|||
} |
|||
|
|||
shift(): E | undefined { |
|||
if (this._first === Node.Undefined) { |
|||
return undefined |
|||
} else { |
|||
const res = this._first.element |
|||
this._remove(this._first) |
|||
return res |
|||
} |
|||
} |
|||
|
|||
pop(): E | undefined { |
|||
if (this._last === Node.Undefined) { |
|||
return undefined |
|||
} else { |
|||
const res = this._last.element |
|||
this._remove(this._last) |
|||
return res |
|||
} |
|||
} |
|||
|
|||
private _remove(node: Node<E>): void { |
|||
if (node.prev !== Node.Undefined && node.next !== Node.Undefined) { |
|||
// middle
|
|||
const anchor = node.prev |
|||
anchor.next = node.next |
|||
node.next.prev = anchor |
|||
} else if (node.prev === Node.Undefined && node.next === Node.Undefined) { |
|||
// only node
|
|||
this._first = Node.Undefined |
|||
this._last = Node.Undefined |
|||
} else if (node.next === Node.Undefined) { |
|||
// last
|
|||
this._last = this._last.prev! |
|||
this._last.next = Node.Undefined |
|||
} else if (node.prev === Node.Undefined) { |
|||
// first
|
|||
this._first = this._first.next! |
|||
this._first.prev = Node.Undefined |
|||
} |
|||
|
|||
// done
|
|||
this._size -= 1 |
|||
} |
|||
|
|||
*[Symbol.iterator](): Iterator<E> { |
|||
let node = this._first |
|||
while (node !== Node.Undefined) { |
|||
yield node.element |
|||
node = node.next |
|||
} |
|||
} |
|||
} |
@ -1,63 +0,0 @@ |
|||
import { app, dialog } from "electron" |
|||
import { inject } from "inversify" |
|||
import Tabs from "main/modules/tabs" |
|||
import WindowManager from "main/modules/window-manager" |
|||
|
|||
export default class BasicCommand { |
|||
constructor( |
|||
@inject(WindowManager) private _WindowManager: WindowManager, |
|||
@inject(Tabs) private _Tabs: Tabs, |
|||
) { |
|||
//
|
|||
} |
|||
|
|||
toggleDevTools() { |
|||
const focusedWindow = this._WindowManager.getFocusWindow() |
|||
if (focusedWindow) { |
|||
// @ts-ignore ...
|
|||
focusedWindow.toggleDevTools() |
|||
} |
|||
} |
|||
fullscreen() { |
|||
const focusedWindow = this._WindowManager.getFocusWindow() |
|||
const isFullScreen = focusedWindow!.isFullScreen() |
|||
focusedWindow!.setFullScreen(!isFullScreen) |
|||
return !isFullScreen |
|||
} |
|||
isFullscreen() { |
|||
const focusedWindow = this._WindowManager.getFocusWindow() |
|||
return focusedWindow!.isFullScreen() |
|||
} |
|||
|
|||
relunch() { |
|||
app.relaunch() |
|||
app.exit() |
|||
} |
|||
|
|||
reload() { |
|||
const focusedWindow = this._WindowManager.getFocusWindow() |
|||
// 重载之后, 刷新并关闭所有的次要窗体
|
|||
if (this._WindowManager.length() > 1 && focusedWindow && focusedWindow.$$opts!.name === this._WindowManager.mainInfo.name) { |
|||
const choice = dialog.showMessageBoxSync(focusedWindow, { |
|||
type: "question", |
|||
buttons: ["取消", "是的,继续", "不,算了"], |
|||
title: "警告", |
|||
defaultId: 2, |
|||
cancelId: 0, |
|||
message: "警告", |
|||
detail: "重载主窗口将关闭所有子窗口,是否继续", |
|||
}) |
|||
if (choice == 1) { |
|||
this._WindowManager.getWndows().forEach(win => { |
|||
if (win.$$opts!.name !== this._WindowManager.mainInfo.name) { |
|||
win.close() |
|||
} |
|||
}) |
|||
} else { |
|||
return |
|||
} |
|||
} |
|||
this._Tabs.closeAll() |
|||
focusedWindow!.reload() |
|||
} |
|||
} |
@ -1,66 +0,0 @@ |
|||
import { inject } from "inversify" |
|||
import Tabs from "main/modules/tabs" |
|||
import WindowManager from "main/modules/window-manager" |
|||
import { broadcast } from "main/utils" |
|||
|
|||
class TabsCommand { |
|||
constructor( |
|||
@inject(Tabs) private _Tabs: Tabs, |
|||
@inject(WindowManager) private _WindowManager: WindowManager, |
|||
) { |
|||
this.listenerTabActive = this.listenerTabActive.bind(this) |
|||
this._Tabs.events.on("update", this.listenerTabActive) |
|||
} |
|||
|
|||
listenerTabActive() { |
|||
broadcast("main:TabsCommand.update", this.getAllTabs()) |
|||
} |
|||
|
|||
bindElement(rect) { |
|||
this._Tabs.updateRect(rect) |
|||
} |
|||
|
|||
reload() { |
|||
this._WindowManager.getMainWindow()?.reload() |
|||
} |
|||
|
|||
sync() { |
|||
this.listenerTabActive() |
|||
if (!this.getAllTabs().length) { |
|||
this.add("about:blank") |
|||
} |
|||
} |
|||
|
|||
add(url) { |
|||
this._Tabs.add(url, true, this._WindowManager.getMainWindow()!) |
|||
} |
|||
|
|||
nagivate(index: number, url: string) { |
|||
this._Tabs.navigate(+index, url) |
|||
} |
|||
|
|||
closeAll() { |
|||
this._Tabs.closeAll() |
|||
} |
|||
|
|||
setActive(index) { |
|||
this._Tabs.changeActive(index) |
|||
} |
|||
|
|||
closeTab(e) { |
|||
this._Tabs.remove(e.body.active) |
|||
} |
|||
|
|||
getAllTabs() { |
|||
return this._Tabs._tabs.map(v => ({ |
|||
url: v.url, |
|||
showUrl: v.showUrl, |
|||
title: v.title, |
|||
favicons: v.favicons, |
|||
isActive: v.isActive, |
|||
})) |
|||
} |
|||
} |
|||
|
|||
export { TabsCommand } |
|||
export default TabsCommand |
@ -1,15 +0,0 @@ |
|||
import { Container, ContainerModule } from "inversify" |
|||
import BasicCommand from "./BasicCommand" |
|||
import TabsCommand from "./TabsCommand" |
|||
|
|||
const modules = new ContainerModule(bind => { |
|||
bind("BasicCommand").to(BasicCommand).inSingletonScope() |
|||
bind("TabsCommand").to(TabsCommand).inSingletonScope() |
|||
}) |
|||
|
|||
async function destroyAllCommand(ioc: Container) { |
|||
await ioc.unloadAsync(modules) |
|||
} |
|||
|
|||
export { modules, destroyAllCommand } |
|||
export default modules |
@ -0,0 +1,60 @@ |
|||
import debug from "debug" |
|||
import { app } from "electron" |
|||
import path from "node:path" |
|||
import logger from "logger/main" |
|||
import * as rfs from "rotating-file-stream" |
|||
import fs from "fs" |
|||
|
|||
// 配置根目录
|
|||
const logsPath = app.getPath("logs") |
|||
logger.debug(`日志地址:${logsPath}`) |
|||
|
|||
const LOG_ROOT = path.join(logsPath) |
|||
|
|||
// 缓存当前应用启动的日志文件流
|
|||
let currentLogStream: rfs.RotatingFileStream | null = null |
|||
|
|||
// 生成当前启动时的日志文件名
|
|||
const getLogFileName = () => { |
|||
const now = new Date() |
|||
const timestamp = now.toISOString().replace(/[:.]/g, "-") |
|||
return `app-${timestamp}.log` |
|||
} |
|||
|
|||
// 覆盖 debug.log 方法
|
|||
const originalLog = debug.log |
|||
debug.log = function (...args) { |
|||
// 保留原始控制台输出
|
|||
originalLog.apply(this, args) |
|||
|
|||
// 确保日志目录存在
|
|||
if (!fs.existsSync(LOG_ROOT)) { |
|||
fs.mkdirSync(LOG_ROOT, { recursive: true }) |
|||
} |
|||
|
|||
// 延迟初始化日志流,直到第一次写入
|
|||
if (!currentLogStream) { |
|||
const logFileName = getLogFileName() |
|||
currentLogStream = rfs.createStream(logFileName, { |
|||
path: LOG_ROOT, |
|||
size: "10M", // 单个文件最大 10MB
|
|||
rotate: 10, // 保留最近 10 个文件
|
|||
}) |
|||
} |
|||
|
|||
// @ts-ignore 获取当前命名空间
|
|||
const namespace = this.namespace || "unknown" |
|||
|
|||
// 写入日志(添加时间戳和命名空间)
|
|||
const timestamp = new Date().toISOString() |
|||
const message = args.join(" ") |
|||
currentLogStream.write(`[${timestamp}] [${namespace}] ${message}\n`) |
|||
} |
|||
|
|||
app.on("before-quit", () => { |
|||
if (currentLogStream) { |
|||
currentLogStream.end() |
|||
currentLogStream.destroy() |
|||
currentLogStream = null |
|||
} |
|||
}) |
@ -0,0 +1,6 @@ |
|||
import EventEmitter from "events" |
|||
|
|||
const globalEvent = new EventEmitter() |
|||
|
|||
export default globalEvent |
|||
export { globalEvent as eventbus } |
@ -1,235 +0,0 @@ |
|||
import fs from "fs-extra" |
|||
import { app } from "electron" |
|||
import path from "path" |
|||
import { cloneDeep } from "lodash" |
|||
import { injectable } from "inversify" |
|||
import Config from "config" |
|||
import _debug from "debug" |
|||
import BaseClass from "main/base/base" |
|||
|
|||
const debug = _debug("app:setting") |
|||
|
|||
type IConfig = typeof Config.default_config |
|||
|
|||
type IOnFunc = (n: IConfig, c: IConfig, keys?: (keyof IConfig)[]) => void |
|||
type IT = (keyof IConfig)[] | keyof IConfig | "_" |
|||
|
|||
let storagePath = path.join(app.getPath("documents"), Config.app_title) |
|||
const storagePathDev = path.join(app.getPath("documents"), Config.app_title + "-dev") |
|||
|
|||
if (process.env.NODE_ENV === "development") { |
|||
storagePath = storagePathDev |
|||
} |
|||
|
|||
const _tempConfig = cloneDeep(Config.default_config as IConfig) |
|||
Object.keys(_tempConfig).forEach(key => { |
|||
if (typeof _tempConfig[key] === "string" && _tempConfig[key].includes("$storagePath$")) { |
|||
_tempConfig[key] = _tempConfig[key].replace(/\$storagePath\$/g, storagePath) |
|||
if (_tempConfig[key] && path.isAbsolute(_tempConfig[key])) { |
|||
_tempConfig[key] = path.normalize(_tempConfig[key]) |
|||
} |
|||
} |
|||
}) |
|||
|
|||
function isPath(str) { |
|||
// 使用正则表达式检查字符串是否以斜杠或盘符开头
|
|||
return /^(?:\/|[a-zA-Z]:\\)/.test(str) |
|||
} |
|||
|
|||
function init(config: IConfig) { |
|||
// 在配置初始化后执行
|
|||
Object.keys(config).forEach(key => { |
|||
if (config[key] && isPath(config[key]) && path.isAbsolute(config[key])) { |
|||
fs.ensureDirSync(config[key]) |
|||
} |
|||
}) |
|||
// 在配置初始化后执行
|
|||
// fs.ensureDirSync(config["snippet.storagePath"])
|
|||
// fs.ensureDirSync(config["bookmark.storagePath"])
|
|||
} |
|||
|
|||
// 判断是否是空文件夹
|
|||
function isEmptyDir(fPath: string) { |
|||
const pa = fs.readdirSync(fPath) |
|||
if (pa.length === 0) { |
|||
return true |
|||
} else { |
|||
return false |
|||
} |
|||
} |
|||
|
|||
@injectable() |
|||
class Setting extends BaseClass { |
|||
constructor() { |
|||
super() |
|||
debug(`Setting inited`) |
|||
this.init() |
|||
} |
|||
|
|||
destroy() { |
|||
// TODO
|
|||
} |
|||
|
|||
#cb: [IT, IOnFunc][] = [] |
|||
|
|||
onChange(fn: IOnFunc, that?: any) |
|||
onChange(key: IT, fn: IOnFunc, that?: any) |
|||
onChange(fnOrType: IT | IOnFunc, fnOrThat: IOnFunc | any = null, that: any = null) { |
|||
if (typeof fnOrType === "function") { |
|||
this.#cb.push(["_", fnOrType.bind(fnOrThat)]) |
|||
} else { |
|||
this.#cb.push([fnOrType, fnOrThat.bind(that)]) |
|||
} |
|||
} |
|||
|
|||
#runCB(n: IConfig, c: IConfig, keys: (keyof IConfig)[]) { |
|||
for (let i = 0; i < this.#cb.length; i++) { |
|||
const temp = this.#cb[i] |
|||
const k = temp[0] |
|||
const fn = temp[1] |
|||
if (k === "_") { |
|||
fn(n, c, keys) |
|||
} |
|||
if (typeof k === "string" && keys.includes(k as keyof IConfig)) { |
|||
fn(n, c) |
|||
} |
|||
if (Array.isArray(k) && k.filter(v => keys.indexOf(v) !== -1).length) { |
|||
fn(n, c) |
|||
} |
|||
} |
|||
} |
|||
|
|||
#pathFile: string = |
|||
process.env.NODE_ENV === "development" |
|||
? path.resolve(app.getPath("userData"), "./config_path-dev") |
|||
: path.resolve(app.getPath("userData"), "./config_path") |
|||
#config: IConfig = cloneDeep(_tempConfig) |
|||
#configPath(storagePath?: string): string { |
|||
return path.join(storagePath || this.#config.storagePath, "./config.json") |
|||
} |
|||
/** |
|||
* 读取配置文件变量同步 |
|||
* @param confingPath 配置文件路径 |
|||
*/ |
|||
#syncVar(confingPath?: string) { |
|||
const configFile = this.#configPath(confingPath) |
|||
if (!fs.pathExistsSync(configFile)) { |
|||
fs.ensureFileSync(configFile) |
|||
fs.writeJSONSync(configFile, {}) |
|||
} |
|||
const config = fs.readJSONSync(configFile) as IConfig |
|||
confingPath && (config.storagePath = confingPath) |
|||
// 优先取本地的值
|
|||
for (const key in config) { |
|||
// if (Object.prototype.hasOwnProperty.call(this.#config, key)) {
|
|||
// this.#config[key] = config[key] || this.#config[key]
|
|||
// }
|
|||
// 删除配置时本地的配置不会改变,想一下哪种方式更好
|
|||
this.#config[key] = config[key] || this.#config[key] |
|||
} |
|||
} |
|||
init() { |
|||
debug(`位置:${this.#pathFile}`) |
|||
|
|||
if (fs.pathExistsSync(this.#pathFile)) { |
|||
const confingPath = fs.readFileSync(this.#pathFile, { encoding: "utf8" }) |
|||
if (confingPath && fs.pathExistsSync(this.#configPath(confingPath))) { |
|||
this.#syncVar(confingPath) |
|||
// 防止增加了配置本地却没变的情况
|
|||
this.#sync(confingPath) |
|||
} else { |
|||
this.#syncVar(confingPath) |
|||
this.#sync(confingPath) |
|||
} |
|||
} else { |
|||
this.#syncVar() |
|||
this.#sync() |
|||
} |
|||
init.call(this, this.#config) |
|||
} |
|||
config() { |
|||
return this.#config |
|||
} |
|||
#sync(c?: string) { |
|||
const config = cloneDeep(this.#config) |
|||
delete config.storagePath |
|||
const p = this.#configPath(c) |
|||
fs.ensureFileSync(p) |
|||
fs.writeJSONSync(this.#configPath(c), config) |
|||
} |
|||
#change(p: string) { |
|||
const storagePath = this.#config.storagePath |
|||
if (fs.existsSync(storagePath) && !fs.existsSync(p)) { |
|||
fs.moveSync(storagePath, p) |
|||
} |
|||
if (fs.existsSync(p) && fs.existsSync(storagePath) && isEmptyDir(p)) { |
|||
fs.moveSync(storagePath, p, { overwrite: true }) |
|||
} |
|||
fs.writeFileSync(this.#pathFile, p, { encoding: "utf8" }) |
|||
} |
|||
reset(key: keyof IConfig) { |
|||
this.set(key, cloneDeep(_tempConfig[key])) |
|||
} |
|||
set(key: keyof IConfig | Partial<IConfig>, value?: any) { |
|||
const oldMainConfig = Object.assign({}, this.#config) |
|||
let isChange = false |
|||
const changeKeys: (keyof IConfig)[] = [] |
|||
const canChangeStorage = (targetPath: string) => { |
|||
if (fs.existsSync(oldMainConfig.storagePath) && fs.existsSync(targetPath) && !isEmptyDir(targetPath)) { |
|||
if (fs.existsSync(path.join(targetPath, "./config.json"))) { |
|||
return true |
|||
} |
|||
return false |
|||
} |
|||
return true |
|||
} |
|||
if (typeof key === "string") { |
|||
if (value != undefined && value !== this.#config[key]) { |
|||
if (key === "storagePath") { |
|||
if (!canChangeStorage(value)) { |
|||
throw "无法改变存储地址" |
|||
return |
|||
} |
|||
this.#change(value) |
|||
changeKeys.push("storagePath") |
|||
this.#config["storagePath"] = value |
|||
} else { |
|||
changeKeys.push(key) |
|||
this.#config[key as string] = value |
|||
} |
|||
isChange = true |
|||
} |
|||
} else { |
|||
if (key["storagePath"] !== undefined && key["storagePath"] !== this.#config["storagePath"]) { |
|||
if (!canChangeStorage(key["storagePath"])) { |
|||
throw "无法改变存储地址" |
|||
return |
|||
} |
|||
this.#change(key["storagePath"]) |
|||
this.#config["storagePath"] = key["storagePath"] |
|||
changeKeys.push("storagePath") |
|||
isChange = true |
|||
} |
|||
for (const _ in key) { |
|||
if (Object.prototype.hasOwnProperty.call(key, _)) { |
|||
const v = key[_] |
|||
if (v != undefined && _ !== "storagePath" && v !== this.#config[_]) { |
|||
this.#config[_] = v |
|||
changeKeys.push(_ as keyof IConfig) |
|||
isChange = true |
|||
} |
|||
} |
|||
} |
|||
} |
|||
if (isChange) { |
|||
this.#sync() |
|||
this.#runCB(this.#config, oldMainConfig, changeKeys) |
|||
} |
|||
} |
|||
values<T extends keyof IConfig>(key: T): IConfig[T] { |
|||
return this.#config[key] |
|||
} |
|||
} |
|||
|
|||
export default Setting |
|||
export { Setting } |
@ -1,6 +1,6 @@ |
|||
export function Layout(width, height) { |
|||
// Tab布局位置
|
|||
const NavbarHeight = 30 |
|||
const OffsetHeight = NavbarHeight + 100 |
|||
return { x: 0, y: OffsetHeight, width: width, height: height - OffsetHeight } |
|||
// Tab布局位置
|
|||
const NavbarHeight = 30 |
|||
const OffsetHeight = NavbarHeight + 100 |
|||
return { x: 0, y: OffsetHeight, width: width, height: height - OffsetHeight } |
|||
} |
|||
|
@ -1,114 +0,0 @@ |
|||
import pkg from "electron-updater" |
|||
import { app, dialog } from "electron" |
|||
import { injectable } from "inversify" |
|||
import BaseClass from "main/base/base" |
|||
// import { Setting } from "../setting"
|
|||
import _debug from "debug" |
|||
import EventEmitter from "events" |
|||
|
|||
const debug = _debug("app:updater") |
|||
const { autoUpdater } = pkg |
|||
|
|||
@injectable() |
|||
export class Updater extends BaseClass { |
|||
public events = new EventEmitter() |
|||
|
|||
constructor( |
|||
// @inject(Setting) private _Setting: Setting
|
|||
) { |
|||
super() |
|||
|
|||
// 配置自动更新
|
|||
autoUpdater.autoDownload = false |
|||
autoUpdater.autoInstallOnAppQuit = true |
|||
|
|||
// 检查更新错误
|
|||
autoUpdater.on("error", error => { |
|||
debug("Update error:", error) |
|||
}) |
|||
|
|||
// 检查更新
|
|||
autoUpdater.on("checking-for-update", () => { |
|||
debug("Checking for updates...") |
|||
}) |
|||
|
|||
// 有可用更新
|
|||
autoUpdater.on("update-available", info => { |
|||
debug("Update available:", info) |
|||
this.promptUserToUpdate() |
|||
}) |
|||
|
|||
// 没有可用更新
|
|||
autoUpdater.on("update-not-available", info => { |
|||
debug("Update not available:", info) |
|||
}) |
|||
|
|||
// 更新下载进度
|
|||
autoUpdater.on("download-progress", progressObj => { |
|||
debug( |
|||
`Download speed: ${progressObj.bytesPerSecond} - Downloaded ${progressObj.percent}% (${progressObj.transferred}/${progressObj.total})`, |
|||
) |
|||
}) |
|||
|
|||
// 更新下载完成
|
|||
autoUpdater.on("update-downloaded", info => { |
|||
debug("Update downloaded:", info) |
|||
this.promptUserToInstall() |
|||
}) |
|||
} |
|||
|
|||
init() { |
|||
// 定期检查更新
|
|||
this.checkForUpdates() |
|||
setInterval( |
|||
() => { |
|||
this.checkForUpdates() |
|||
}, |
|||
1000 * 60 * 60, |
|||
) // 每小时检查一次
|
|||
} |
|||
|
|||
destroy() { |
|||
// 清理工作
|
|||
} |
|||
|
|||
private async checkForUpdates() { |
|||
if (app.isPackaged) { |
|||
try { |
|||
await autoUpdater.checkForUpdates() |
|||
} catch (error) { |
|||
debug("Failed to check for updates:", error) |
|||
} |
|||
} |
|||
} |
|||
|
|||
private async promptUserToUpdate() { |
|||
const result = await dialog.showMessageBox({ |
|||
type: "info", |
|||
title: "发现新版本", |
|||
message: "是否下载新版本?", |
|||
buttons: ["下载", "暂不更新"], |
|||
defaultId: 0, |
|||
}) |
|||
|
|||
if (result.response === 0) { |
|||
autoUpdater.downloadUpdate() |
|||
} |
|||
} |
|||
|
|||
private async promptUserToInstall() { |
|||
const result = await dialog.showMessageBox({ |
|||
type: "info", |
|||
title: "更新已就绪", |
|||
message: "新版本已下载完成,是否立即安装?", |
|||
buttons: ["立即安装", "稍后安装"], |
|||
defaultId: 0, |
|||
}) |
|||
|
|||
if (result.response === 0) { |
|||
autoUpdater.quitAndInstall(false, true) |
|||
} |
|||
} |
|||
} |
|||
|
|||
export default Updater |
@ -1,130 +1,107 @@ |
|||
import config from "config" |
|||
import { BrowserWindowConstructorOptions } from "electron" |
|||
import { getFileUrl } from "main/utils" |
|||
import icon from "res/icon.png?asset" |
|||
import { join } from "path" |
|||
import { getFileUrl, getPreloadUrl } from "main/utils" |
|||
import icon from "@res/icon.png?asset" |
|||
|
|||
export type Param = Partial<IConfig> & Required<Pick<IConfig, "name">> |
|||
|
|||
export interface IConfig { |
|||
name?: string |
|||
url?: string |
|||
loadURLInSameWin?: boolean |
|||
type?: "info" |
|||
windowOpts?: BrowserWindowConstructorOptions |
|||
overideWindowOpts?: boolean |
|||
ignoreEmptyUrl?: boolean |
|||
denyWindowOpen?: boolean |
|||
confrimWindowClose?: boolean |
|||
confrimWindowCloseText?: { |
|||
title: string |
|||
message: string |
|||
buttons: string[] |
|||
defaultId: number |
|||
cancelId: number |
|||
} |
|||
name?: string |
|||
url?: string |
|||
loadURLInSameWin?: boolean |
|||
type?: "info" |
|||
windowOpts?: BrowserWindowConstructorOptions |
|||
overideWindowOpts?: boolean |
|||
ignoreEmptyUrl?: boolean |
|||
denyWindowOpen?: boolean |
|||
confrimWindowClose?: boolean |
|||
confrimWindowCloseText?: { |
|||
title: string |
|||
message: string |
|||
buttons: string[] |
|||
defaultId: number |
|||
cancelId: number |
|||
} |
|||
} |
|||
|
|||
export const defaultConfig: IConfig = { |
|||
denyWindowOpen: true, |
|||
denyWindowOpen: true, |
|||
} |
|||
|
|||
export const defaultWindowConfig = { |
|||
height: 600, |
|||
useContentSize: true, |
|||
width: 800, |
|||
show: true, |
|||
resizable: true, |
|||
minWidth: 900, |
|||
minHeight: 600, |
|||
frame: true, |
|||
transparent: false, |
|||
alwaysOnTop: false, |
|||
webPreferences: {}, |
|||
height: 600, |
|||
useContentSize: true, |
|||
width: 800, |
|||
show: true, |
|||
resizable: true, |
|||
minWidth: 900, |
|||
minHeight: 600, |
|||
frame: true, |
|||
transparent: false, |
|||
alwaysOnTop: false, |
|||
webPreferences: {}, |
|||
} |
|||
|
|||
export function getWindowsMap(): Record<string, IConfig> { |
|||
return { |
|||
main: { |
|||
name: "main", |
|||
url: getFileUrl("index.html"), |
|||
confrimWindowClose: true, |
|||
confrimWindowCloseText: { |
|||
title: config.app_title, |
|||
defaultId: 0, |
|||
cancelId: 0, |
|||
message: "确定要关闭吗?", |
|||
buttons: ["没事", "直接退出"], |
|||
}, |
|||
windowOpts: { |
|||
show: false, |
|||
titleBarStyle: "hidden", |
|||
titleBarOverlay: true, |
|||
icon: icon, |
|||
...(process.platform === "linux" ? { icon } : {}), |
|||
webPreferences: { |
|||
webviewTag: false, |
|||
preload: join(__dirname, "../preload/index.mjs"), |
|||
nodeIntegration: true, |
|||
contextIsolation: true, |
|||
}, |
|||
}, |
|||
return { |
|||
main: { |
|||
name: "main", |
|||
url: getFileUrl("index.html"), |
|||
confrimWindowClose: true, |
|||
confrimWindowCloseText: { |
|||
title: config.app_title, |
|||
defaultId: 0, |
|||
cancelId: 0, |
|||
message: "确定要关闭吗?", |
|||
buttons: ["没事", "直接退出"], |
|||
}, |
|||
windowOpts: { |
|||
show: false, |
|||
titleBarStyle: "hidden", |
|||
titleBarOverlay: true, |
|||
icon: icon, |
|||
...(process.platform === "linux" ? { icon } : {}), |
|||
webPreferences: { |
|||
webviewTag: false, |
|||
preload: getPreloadUrl("index"), |
|||
nodeIntegration: true, |
|||
contextIsolation: true, |
|||
}, |
|||
_blank: { |
|||
overideWindowOpts: false, |
|||
confrimWindowClose: true, |
|||
confrimWindowCloseText: { |
|||
title: config.app_title, |
|||
defaultId: 0, |
|||
cancelId: 0, |
|||
message: "确定要关闭吗?", |
|||
buttons: ["没事", "直接退出"], |
|||
}, |
|||
type: "info", |
|||
windowOpts: { |
|||
height: 600, |
|||
useContentSize: true, |
|||
width: 800, |
|||
show: true, |
|||
resizable: true, |
|||
minWidth: 900, |
|||
minHeight: 600, |
|||
frame: true, |
|||
transparent: false, |
|||
alwaysOnTop: false, |
|||
icon: icon, |
|||
title: config.app_title, |
|||
webPreferences: { |
|||
devTools: false, |
|||
sandbox: true, |
|||
nodeIntegration: false, |
|||
contextIsolation: true, |
|||
webviewTag: false, |
|||
preload: undefined, |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
_blank: { |
|||
overideWindowOpts: false, |
|||
confrimWindowClose: true, |
|||
confrimWindowCloseText: { |
|||
title: config.app_title, |
|||
defaultId: 0, |
|||
cancelId: 0, |
|||
message: "确定要关闭吗?", |
|||
buttons: ["没事", "直接退出"], |
|||
}, |
|||
type: "info", |
|||
windowOpts: { |
|||
height: 600, |
|||
useContentSize: true, |
|||
width: 800, |
|||
show: true, |
|||
resizable: true, |
|||
minWidth: 900, |
|||
minHeight: 600, |
|||
frame: true, |
|||
transparent: false, |
|||
alwaysOnTop: false, |
|||
icon: icon, |
|||
title: config.app_title, |
|||
webPreferences: { |
|||
devTools: false, |
|||
sandbox: true, |
|||
nodeIntegration: false, |
|||
contextIsolation: true, |
|||
webviewTag: false, |
|||
preload: undefined, |
|||
}, |
|||
"^about": { |
|||
url: getFileUrl("about.html"), |
|||
overideWindowOpts: true, |
|||
confrimWindowClose: false, |
|||
type: "info", |
|||
windowOpts: { |
|||
width: 600, |
|||
height: 200, |
|||
minimizable: false, |
|||
darkTheme: true, |
|||
modal: true, |
|||
show: false, |
|||
resizable: false, |
|||
icon: icon, |
|||
webPreferences: { |
|||
devTools: false, |
|||
sandbox: false, |
|||
nodeIntegration: false, |
|||
contextIsolation: true, |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
}, |
|||
}, |
|||
} |
|||
} |
|||
|
@ -0,0 +1,47 @@ |
|||
import { BrowserView, BrowserWindow } from "electron" |
|||
|
|||
const cookies = { |
|||
getCurrCookies(params = {}, currWin: BrowserView | BrowserWindow) { |
|||
let currSession = currWin.webContents.session |
|||
return currSession.cookies.get(Object.assign({}, params)) |
|||
}, |
|||
removeCurrCookies(cookies = [], currWin: BrowserView | BrowserWindow) { |
|||
let currSession = currWin.webContents.session |
|||
let err = [] |
|||
let apiCount = 0 |
|||
return new Promise((resove, reject) => { |
|||
cookies.forEach(async (item: any) => { |
|||
await currSession.cookies.remove(`http://${item.domain}`, item.name) |
|||
apiCount = apiCount + 1 |
|||
if (err.length === apiCount) { |
|||
resove({ message: "cookie 清除成功" }) |
|||
} else { |
|||
reject(err) |
|||
} |
|||
}) |
|||
}) |
|||
}, |
|||
setCurrCookies(cookies = [], currWin: BrowserView | BrowserWindow) { |
|||
let currSession = currWin.webContents.session |
|||
let err = [] |
|||
let apiCount = 0 |
|||
return new Promise((resove, reject) => { |
|||
cookies.forEach(async (item: any) => { |
|||
await currSession.cookies.set( |
|||
Object.assign({}, item, { |
|||
url: `http://${item.domain}`, |
|||
name: item.name, |
|||
}), |
|||
) |
|||
apiCount = apiCount + 1 |
|||
if (err.length === apiCount) { |
|||
resove({ message: "cookie 设置成功!" }) |
|||
} else { |
|||
reject(err) |
|||
} |
|||
}) |
|||
}) |
|||
}, |
|||
} |
|||
|
|||
export default cookies |
@ -0,0 +1,82 @@ |
|||
// https://blog.guowenfh.com/2017/10/21/2017/electron-multiple-session/#%E5%9C%A8-webview-%E4%B8%AD
|
|||
|
|||
import { BrowserWindow } from "electron" |
|||
/** |
|||
* 创建一个 登录 的窗口。 |
|||
* 用于 session 隔离 |
|||
* Promise 中有 {partition,userinfo,cookies} |
|||
* @returns Promise |
|||
*/ |
|||
function createLoginWin(partition) { |
|||
partition = partition || `persist:${Math.random()}` |
|||
// const charset = require("superagent-charset")
|
|||
// const request = charset(require("superagent")) // HTTP
|
|||
let presWindow = new BrowserWindow({ |
|||
width: 1280, |
|||
height: 768, |
|||
title: "用户登陆", |
|||
webPreferences: { |
|||
webSecurity: false, |
|||
allowRunningInsecureContent: true, |
|||
partition, |
|||
}, |
|||
}) |
|||
let webContents = presWindow.webContents |
|||
return new Promise(function (resove, _) { |
|||
// webContents.openDevTools();
|
|||
presWindow.loadURL("https://login.taobao.com/member/login.jhtml") |
|||
webContents.on("did-navigate-in-page", async function () { |
|||
// 这里可以看情况进行参数的传递,获取制定的 cookies
|
|||
const cookies = await webContents.session.cookies.get({}) |
|||
let obj = { partition, cookies } |
|||
resove(obj) |
|||
// webContents.session.cookies.get({}, function (err, cookies) {
|
|||
// if (err) {
|
|||
// presWindow.close() // 关闭登陆窗口
|
|||
// return reject(err)
|
|||
// }
|
|||
// let obj = { partition, cookies }
|
|||
// resove(obj)
|
|||
// fetch("https://login.taobao.com/member/login.jhtml", {
|
|||
// method: "GET",
|
|||
// credentials: "include",
|
|||
// headers: {
|
|||
// Cookie: cookies.map(item => `${item.name}=${item.value};`).join(" "),
|
|||
// "Content-Type": "application/json",
|
|||
// },
|
|||
// })
|
|||
// .then(response => response.json())
|
|||
// .then(data => {
|
|||
// console.log(data)
|
|||
// presWindow.close()
|
|||
// resove(obj)
|
|||
// })
|
|||
// .catch(err => {
|
|||
// presWindow.close()
|
|||
// reject(err)
|
|||
// })
|
|||
// })
|
|||
// 这一步并不是必需的。
|
|||
// request
|
|||
// .get("http://taobao.com/userinfo")
|
|||
// .query({ _: Date.now() }) // query string
|
|||
// .set("Cookie", cookies.map(item => `${item.name}=${item.value};`).join(" "))
|
|||
// .end(function (err, res) {
|
|||
// presWindow.close()
|
|||
// if (err) {
|
|||
// return reject(err)
|
|||
// }
|
|||
// if (!res || !res.body || !res.body.result !== 1) {
|
|||
// return reject(res.body)
|
|||
// }
|
|||
// let obj = { partition, cookies, userinfo: res.body.data }
|
|||
// resove(obj)
|
|||
// })
|
|||
}) |
|||
// })
|
|||
}) |
|||
} |
|||
|
|||
export { |
|||
createLoginWin |
|||
} |
@ -1,8 +0,0 @@ |
|||
import { ElectronAPI } from "@electron-toolkit/preload" |
|||
|
|||
declare global { |
|||
interface Window { |
|||
electron: ElectronAPI |
|||
api: unknown |
|||
} |
|||
} |
@ -1,28 +1,32 @@ |
|||
<!doctype html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8" /> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
|||
<title>关于我</title> |
|||
<style> |
|||
html, |
|||
body { |
|||
height: 100%; |
|||
width: 100%; |
|||
margin: 0; |
|||
padding: 0; |
|||
outline: none; |
|||
border: 0; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
} |
|||
</style> |
|||
</head> |
|||
|
|||
<body> |
|||
<article> |
|||
<h1 id="demo" style="text-align: center">您好,亲爱的冒险者!</h1> |
|||
</article> |
|||
</body> |
|||
</html> |
|||
<head> |
|||
<meta charset="UTF-8" /> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
|||
<title>关于我</title> |
|||
<style> |
|||
html, |
|||
body { |
|||
height: 100%; |
|||
width: 100%; |
|||
margin: 0; |
|||
padding: 0; |
|||
outline: none; |
|||
border: 0; |
|||
overflow: hidden; |
|||
padding: 10px 20px; |
|||
} |
|||
</style> |
|||
</head> |
|||
|
|||
<body> |
|||
<article> |
|||
<h1>环境</h1> |
|||
<ul> |
|||
<li>111</li> |
|||
</ul> |
|||
</article> |
|||
</body> |
|||
|
|||
</html> |
@ -1,26 +1,27 @@ |
|||
<!doctype html> |
|||
<html> |
|||
|
|||
<head> |
|||
<head> |
|||
<meta charset="UTF-8" /> |
|||
<title>Electron</title> |
|||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> |
|||
<meta http-equiv="Content-Security-Policy" content="default-src 'self' api: 'unsafe-inline'; |
|||
<meta |
|||
http-equiv="Content-Security-Policy" |
|||
content="default-src 'self' api: 'unsafe-inline'; |
|||
script-src 'self' api:; |
|||
style-src 'self' 'unsafe-inline'; |
|||
img-src 'self' data: *;" /> |
|||
</head> |
|||
img-src 'self' data: *;" |
|||
/> |
|||
</head> |
|||
|
|||
<body> |
|||
<body> |
|||
<div id="app"></div> |
|||
<noscript> |
|||
<style> |
|||
[data-simplebar] { |
|||
overflow: auto; |
|||
} |
|||
</style> |
|||
<style> |
|||
[data-simplebar] { |
|||
overflow: auto; |
|||
} |
|||
</style> |
|||
</noscript> |
|||
<script type="module" src="/src/main.ts"></script> |
|||
</body> |
|||
|
|||
</body> |
|||
</html> |
|||
|
@ -1,9 +1,32 @@ |
|||
<script setup lang="ts"></script> |
|||
<script setup lang="ts"> |
|||
|
|||
</script> |
|||
|
|||
<template> |
|||
<router-view v-slot="{ Component, route }"> |
|||
<transition name="slide"> |
|||
<component :is="Component" :key="route" /> |
|||
</transition> |
|||
</router-view> |
|||
<div h-full flex flex-col overflow-hidden> |
|||
<NavBar></NavBar> |
|||
<div flex-1 h-0 overflow-hidden flex flex-col relative id="page-container" style="transform: scale(1);"> |
|||
<router-view v-slot="{ Component, route }"> |
|||
<Transition name="slide-fade" mode="out-in"> |
|||
<component :is="Component" :key="route.fullPath" /> |
|||
</Transition> |
|||
</router-view> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<style lang="scss" scoped> |
|||
.slide-fade-enter-active { |
|||
transition: all 0.2s ease-out; |
|||
} |
|||
|
|||
.slide-fade-leave-active { |
|||
transition: all 0.2s cubic-bezier(1, 0.5, 0.8, 1); |
|||
} |
|||
|
|||
.slide-fade-enter-from, |
|||
.slide-fade-leave-to { |
|||
// transform: translateX(20px); |
|||
opacity: 0; |
|||
} |
|||
</style> |
|||
|
@ -1,246 +1,245 @@ |
|||
interface ScrollStyle { |
|||
[key: string]: string |
|||
[key: string]: string |
|||
} |
|||
|
|||
class Scrollbot { |
|||
private orgPar!: HTMLElement |
|||
private sbw: number = 5 |
|||
private scrollSpeed: number = 200 |
|||
private parContent!: string |
|||
private newPar!: HTMLDivElement |
|||
// private sbContainer!: HTMLDivElement
|
|||
private scrollBarHolder!: HTMLDivElement |
|||
private scrollBar!: HTMLDivElement |
|||
private inP!: HTMLDivElement |
|||
private sbHeight: number = 0 |
|||
private mdown: boolean = false |
|||
private customHeight: boolean = false |
|||
// private scrollElement!: HTMLElement
|
|||
private onScrollF?: () => void |
|||
private sB: ScrollStyle = {} |
|||
private sBH: ScrollStyle = {} |
|||
private posCorrection: number = 0 |
|||
private btmCorrection: number = 0 |
|||
private relY: number = 0 |
|||
private pC: number = 0 |
|||
|
|||
getDom(selector: string | HTMLElement) { |
|||
if (typeof selector === "string") { |
|||
return document.querySelector<HTMLElement>(selector) |
|||
} |
|||
return selector |
|||
private orgPar!: HTMLElement |
|||
private sbw: number = 5 |
|||
private scrollSpeed: number = 200 |
|||
private parContent!: string |
|||
private newPar!: HTMLDivElement |
|||
// private sbContainer!: HTMLDivElement
|
|||
private scrollBarHolder!: HTMLDivElement |
|||
private scrollBar!: HTMLDivElement |
|||
private inP!: HTMLDivElement |
|||
private sbHeight: number = 0 |
|||
private mdown: boolean = false |
|||
private customHeight: boolean = false |
|||
// private scrollElement!: HTMLElement
|
|||
private onScrollF?: () => void |
|||
private sB: ScrollStyle = {} |
|||
private sBH: ScrollStyle = {} |
|||
private posCorrection: number = 0 |
|||
private btmCorrection: number = 0 |
|||
private relY: number = 0 |
|||
private pC: number = 0 |
|||
|
|||
getDom(selector: string | HTMLElement) { |
|||
if (typeof selector === "string") { |
|||
return document.querySelector<HTMLElement>(selector) |
|||
} |
|||
return selector |
|||
} |
|||
|
|||
constructor(selector: string | HTMLElement, width?: number) { |
|||
const element = this.getDom(selector) |
|||
if (!element) throw new Error("Element not found") |
|||
this.orgPar = element |
|||
constructor(selector: string | HTMLElement, width?: number) { |
|||
const element = this.getDom(selector) |
|||
if (!element) throw new Error("Element not found") |
|||
this.orgPar = element |
|||
|
|||
const ieVersion = this.isIE() |
|||
if (!ieVersion || (ieVersion && ieVersion < 9)) { |
|||
this.init(width) |
|||
} |
|||
const ieVersion = this.isIE() |
|||
if (!ieVersion || (ieVersion && ieVersion < 9)) { |
|||
this.init(width) |
|||
} |
|||
|
|||
private init(width?: number): void { |
|||
this.sbw = width ?? 5 |
|||
this.parContent = this.orgPar.innerHTML |
|||
this.orgPar.innerHTML = "" |
|||
|
|||
this.setupElements() |
|||
this.setupStyles() |
|||
this.setupEventListeners() |
|||
this.refresh() |
|||
} |
|||
|
|||
private init(width?: number): void { |
|||
this.sbw = width ?? 5 |
|||
this.parContent = this.orgPar.innerHTML |
|||
this.orgPar.innerHTML = "" |
|||
|
|||
this.setupElements() |
|||
this.setupStyles() |
|||
this.setupEventListeners() |
|||
this.refresh() |
|||
} |
|||
|
|||
private setupElements(): void { |
|||
this.newPar = document.createElement("div") |
|||
// this.sbContainer = document.createElement("div")
|
|||
this.scrollBarHolder = document.createElement("div") |
|||
this.scrollBar = document.createElement("div") |
|||
this.inP = document.createElement("div") |
|||
|
|||
this.newPar.className = "scrollbot-outer-parent" |
|||
this.scrollBarHolder.className = "scrollbot-scrollbar-holder" |
|||
this.scrollBar.className = "scrollbot-scrollbar" |
|||
this.inP.className = "scrollbot-inner-parent" |
|||
|
|||
this.inP.innerHTML = this.parContent |
|||
this.newPar.appendChild(this.inP) |
|||
this.scrollBarHolder.appendChild(this.scrollBar) |
|||
this.newPar.appendChild(this.scrollBarHolder) |
|||
this.orgPar.appendChild(this.newPar) |
|||
} |
|||
|
|||
private setupStyles(): void { |
|||
this.newPar.style.position = "relative" |
|||
this.newPar.style.paddingRight = `${this.sbw}px` |
|||
this.newPar.style.zIndex = "9999999" |
|||
this.newPar.style.height = "100%" |
|||
this.newPar.style.overflow = "hidden" |
|||
|
|||
this.inP.style.cssText = `height:100%;overflow-y:auto;overflow-x:hidden;padding-right:${ |
|||
this.sbw + 20 |
|||
}px;width:100%;box-sizing:content-box;` |
|||
|
|||
this.sbHeight = (this.inP.clientHeight * 100) / this.inP.scrollHeight |
|||
// this.scrollElement = this.inP
|
|||
|
|||
this.updateScrollbarStyles() |
|||
} |
|||
|
|||
private updateScrollbarStyles(): void { |
|||
this.sB = { |
|||
width: `${this.sbw}px`, |
|||
height: `${this.sbHeight}%`, |
|||
position: "absolute", |
|||
right: "0", |
|||
top: "0", |
|||
backgroundColor: "#444444", |
|||
borderRadius: "15px", |
|||
} |
|||
|
|||
private setupElements(): void { |
|||
this.newPar = document.createElement("div") |
|||
// this.sbContainer = document.createElement("div")
|
|||
this.scrollBarHolder = document.createElement("div") |
|||
this.scrollBar = document.createElement("div") |
|||
this.inP = document.createElement("div") |
|||
|
|||
this.newPar.className = "scrollbot-outer-parent" |
|||
this.scrollBarHolder.className = "scrollbot-scrollbar-holder" |
|||
this.scrollBar.className = "scrollbot-scrollbar" |
|||
this.inP.className = "scrollbot-inner-parent" |
|||
|
|||
this.inP.innerHTML = this.parContent |
|||
this.newPar.appendChild(this.inP) |
|||
this.scrollBarHolder.appendChild(this.scrollBar) |
|||
this.newPar.appendChild(this.scrollBarHolder) |
|||
this.orgPar.appendChild(this.newPar) |
|||
} |
|||
|
|||
private setupStyles(): void { |
|||
this.newPar.style.position = "relative" |
|||
this.newPar.style.paddingRight = `${this.sbw}px` |
|||
this.newPar.style.zIndex = "9999999" |
|||
this.newPar.style.height = "100%" |
|||
this.newPar.style.overflow = "hidden" |
|||
|
|||
this.inP.style.cssText = `height:100%;overflow-y:auto;overflow-x:hidden;padding-right:${ |
|||
this.sbw + 20 |
|||
}px;width:100%;box-sizing:content-box;` |
|||
|
|||
this.sbHeight = (this.inP.clientHeight * 100) / this.inP.scrollHeight |
|||
// this.scrollElement = this.inP
|
|||
|
|||
this.updateScrollbarStyles() |
|||
this.sBH = { |
|||
width: `${this.sbw}px`, |
|||
height: "100%", |
|||
position: "absolute", |
|||
right: "0", |
|||
top: "0", |
|||
backgroundColor: "#ADADAD", |
|||
borderRadius: "15px", |
|||
} |
|||
|
|||
private updateScrollbarStyles(): void { |
|||
this.sB = { |
|||
width: `${this.sbw}px`, |
|||
height: `${this.sbHeight}%`, |
|||
position: "absolute", |
|||
right: "0", |
|||
top: "0", |
|||
backgroundColor: "#444444", |
|||
borderRadius: "15px", |
|||
} |
|||
Object.assign(this.scrollBar.style, this.sB) |
|||
Object.assign(this.scrollBarHolder.style, this.sBH) |
|||
} |
|||
|
|||
this.sBH = { |
|||
width: `${this.sbw}px`, |
|||
height: "100%", |
|||
position: "absolute", |
|||
right: "0", |
|||
top: "0", |
|||
backgroundColor: "#ADADAD", |
|||
borderRadius: "15px", |
|||
} |
|||
public refresh(): void { |
|||
this.sbHeight = (this.inP.clientHeight * 100) / this.inP.scrollHeight |
|||
this.scrollBarHolder.style.display = this.sbHeight >= 100 ? "none" : "block" |
|||
|
|||
Object.assign(this.scrollBar.style, this.sB) |
|||
Object.assign(this.scrollBarHolder.style, this.sBH) |
|||
if (this.inP.scrollHeight > this.inP.clientHeight) { |
|||
this.scrollBar.style.height = this.customHeight ? this.sB.height : `${this.sbHeight}%` |
|||
} |
|||
|
|||
public refresh(): void { |
|||
this.sbHeight = (this.inP.clientHeight * 100) / this.inP.scrollHeight |
|||
this.scrollBarHolder.style.display = this.sbHeight >= 100 ? "none" : "block" |
|||
|
|||
if (this.inP.scrollHeight > this.inP.clientHeight) { |
|||
this.scrollBar.style.height = this.customHeight ? this.sB.height : `${this.sbHeight}%` |
|||
} |
|||
} |
|||
|
|||
public destroy(): void { |
|||
this.orgPar.innerHTML = this.parContent |
|||
this.orgPar.style.overflow = "auto" |
|||
} |
|||
|
|||
private isIE(): number | false { |
|||
const userAgent = navigator.userAgent.toLowerCase() |
|||
const msie = userAgent.indexOf("msie") |
|||
return msie !== -1 ? parseInt(userAgent.split("msie")[1]) : false |
|||
} |
|||
|
|||
public onScroll(callback: () => void): void { |
|||
this.onScrollF = callback |
|||
} |
|||
|
|||
private setupEventListeners(): void { |
|||
this.setupScrollListener() |
|||
this.setupMouseEvents() |
|||
} |
|||
|
|||
private setupScrollListener(): void { |
|||
this.inP.addEventListener("scroll", () => { |
|||
const scrollPercentage = (this.inP.scrollTop * 100) / this.inP.scrollHeight |
|||
const correction = |
|||
((this.sbHeight - parseFloat(this.sB.height)) * this.inP.scrollTop) / (this.inP.scrollHeight - this.inP.clientHeight) |
|||
|
|||
this.scrollBar.style.top = `${scrollPercentage + correction}%` |
|||
|
|||
if (this.onScrollF) { |
|||
this.onScrollF() |
|||
} |
|||
}) |
|||
} |
|||
|
|||
private setScroll(position: number, duration: number = 500): void { |
|||
if (position >= this.inP.scrollHeight - this.inP.clientHeight) { |
|||
position = this.inP.scrollHeight - this.inP.clientHeight |
|||
} |
|||
|
|||
public destroy(): void { |
|||
this.orgPar.innerHTML = this.parContent |
|||
this.orgPar.style.overflow = "auto" |
|||
const difference = position - this.inP.scrollTop |
|||
const perTick = (difference / duration) * 10 |
|||
|
|||
setTimeout(() => { |
|||
this.inP.scrollTop += perTick |
|||
if (Math.abs(position - this.inP.scrollTop) < 5) return |
|||
this.setScroll(position, duration - 10) |
|||
}, 10) |
|||
} |
|||
|
|||
private setupMouseEvents(): void { |
|||
// 滚动条容器点击事件
|
|||
this.scrollBarHolder.onmousedown = (e: MouseEvent) => { |
|||
if (e.target !== this.scrollBarHolder) return |
|||
const relPos = ((e.pageY - this.scrollBarHolder.getBoundingClientRect().top) * 100) / this.scrollBarHolder.clientHeight |
|||
this.setScroll((this.inP.scrollHeight * relPos) / 100, this.scrollSpeed) |
|||
} |
|||
|
|||
private isIE(): number | false { |
|||
const userAgent = navigator.userAgent.toLowerCase() |
|||
const msie = userAgent.indexOf("msie") |
|||
return msie !== -1 ? parseInt(userAgent.split("msie")[1]) : false |
|||
// 滚动条拖动事件
|
|||
this.scrollBar.onmousedown = (e: MouseEvent) => { |
|||
this.mdown = true |
|||
this.posCorrection = e.pageY - this.scrollBar.getBoundingClientRect().top |
|||
this.btmCorrection = (this.scrollBar.clientHeight * 100) / this.newPar.clientHeight |
|||
return false |
|||
} |
|||
|
|||
public onScroll(callback: () => void): void { |
|||
this.onScrollF = callback |
|||
// 全局鼠标事件
|
|||
document.onmouseup = () => { |
|||
this.mdown = false |
|||
} |
|||
|
|||
private setupEventListeners(): void { |
|||
this.setupScrollListener() |
|||
this.setupMouseEvents() |
|||
} |
|||
|
|||
private setupScrollListener(): void { |
|||
this.inP.addEventListener("scroll", () => { |
|||
const scrollPercentage = (this.inP.scrollTop * 100) / this.inP.scrollHeight |
|||
const correction = |
|||
((this.sbHeight - parseFloat(this.sB.height)) * this.inP.scrollTop) / (this.inP.scrollHeight - this.inP.clientHeight) |
|||
|
|||
this.scrollBar.style.top = `${scrollPercentage + correction}%` |
|||
|
|||
if (this.onScrollF) { |
|||
this.onScrollF() |
|||
} |
|||
}) |
|||
} |
|||
|
|||
private setScroll(position: number, duration: number = 500): void { |
|||
if (position >= this.inP.scrollHeight - this.inP.clientHeight) { |
|||
position = this.inP.scrollHeight - this.inP.clientHeight |
|||
} |
|||
|
|||
const difference = position - this.inP.scrollTop |
|||
const perTick = (difference / duration) * 10 |
|||
|
|||
setTimeout(() => { |
|||
this.inP.scrollTop += perTick |
|||
if (Math.abs(position - this.inP.scrollTop) < 5) return |
|||
this.setScroll(position, duration - 10) |
|||
}, 10) |
|||
} |
|||
|
|||
private setupMouseEvents(): void { |
|||
// 滚动条容器点击事件
|
|||
this.scrollBarHolder.onmousedown = (e: MouseEvent) => { |
|||
if (e.target !== this.scrollBarHolder) return |
|||
const relPos = ((e.pageY - this.scrollBarHolder.getBoundingClientRect().top) * 100) / this.scrollBarHolder.clientHeight |
|||
this.setScroll((this.inP.scrollHeight * relPos) / 100, this.scrollSpeed) |
|||
} |
|||
|
|||
// 滚动条拖动事件
|
|||
this.scrollBar.onmousedown = (e: MouseEvent) => { |
|||
this.mdown = true |
|||
this.posCorrection = e.pageY - this.scrollBar.getBoundingClientRect().top |
|||
this.btmCorrection = (this.scrollBar.clientHeight * 100) / this.newPar.clientHeight |
|||
return false |
|||
document.onmousemove = (e: MouseEvent) => { |
|||
if (this.mdown) { |
|||
// 清除文本选择
|
|||
window.getSelection()?.removeAllRanges() |
|||
|
|||
this.relY = e.pageY - this.newPar.getBoundingClientRect().top |
|||
this.pC = ((this.relY - this.posCorrection) * 100) / this.newPar.clientHeight |
|||
|
|||
if (this.pC >= 0 && this.pC + this.btmCorrection <= 100) { |
|||
this.scrollBar.style.top = `${this.pC}%` |
|||
this.inP.scrollTop = |
|||
((parseFloat(this.scrollBar.style.top) - |
|||
((this.sbHeight - parseFloat(this.sB.height)) * this.inP.scrollTop) / (this.inP.scrollHeight - this.inP.clientHeight)) * |
|||
this.inP.scrollHeight) / |
|||
100 |
|||
} else if (this.pC < 0 && parseFloat(this.scrollBar.style.top) > 0) { |
|||
this.scrollBar.style.top = "0%" |
|||
this.inP.scrollTop = 0 |
|||
} |
|||
|
|||
// 全局鼠标事件
|
|||
document.onmouseup = () => { |
|||
this.mdown = false |
|||
} |
|||
|
|||
document.onmousemove = (e: MouseEvent) => { |
|||
if (this.mdown) { |
|||
// 清除文本选择
|
|||
window.getSelection()?.removeAllRanges() |
|||
|
|||
this.relY = e.pageY - this.newPar.getBoundingClientRect().top |
|||
this.pC = ((this.relY - this.posCorrection) * 100) / this.newPar.clientHeight |
|||
|
|||
if (this.pC >= 0 && this.pC + this.btmCorrection <= 100) { |
|||
this.scrollBar.style.top = `${this.pC}%` |
|||
this.inP.scrollTop = |
|||
((parseFloat(this.scrollBar.style.top) - |
|||
((this.sbHeight - parseFloat(this.sB.height)) * this.inP.scrollTop) / |
|||
(this.inP.scrollHeight - this.inP.clientHeight)) * |
|||
this.inP.scrollHeight) / |
|||
100 |
|||
} else if (this.pC < 0 && parseFloat(this.scrollBar.style.top) > 0) { |
|||
this.scrollBar.style.top = "0%" |
|||
this.inP.scrollTop = 0 |
|||
} |
|||
|
|||
if (this.onScrollF) { |
|||
this.onScrollF() |
|||
} |
|||
} |
|||
return false |
|||
if (this.onScrollF) { |
|||
this.onScrollF() |
|||
} |
|||
} |
|||
return false |
|||
} |
|||
} |
|||
|
|||
public setStyle(scrollbar?: ScrollStyle, scrollbarHolder?: ScrollStyle): Scrollbot { |
|||
if (scrollbar) { |
|||
scrollbar.width = `${this.sbw}px` |
|||
if ("height" in scrollbar) { |
|||
this.customHeight = true |
|||
scrollbar.height = `${(parseFloat(scrollbar.height) * 100) / this.newPar.clientHeight}%` |
|||
} |
|||
Object.assign(this.sB, scrollbar) |
|||
Object.assign(this.scrollBar.style, scrollbar) |
|||
} |
|||
|
|||
public setStyle(scrollbar?: ScrollStyle, scrollbarHolder?: ScrollStyle): Scrollbot { |
|||
if (scrollbar) { |
|||
scrollbar.width = `${this.sbw}px` |
|||
if ("height" in scrollbar) { |
|||
this.customHeight = true |
|||
scrollbar.height = `${(parseFloat(scrollbar.height) * 100) / this.newPar.clientHeight}%` |
|||
} |
|||
Object.assign(this.sB, scrollbar) |
|||
Object.assign(this.scrollBar.style, scrollbar) |
|||
} |
|||
|
|||
if (scrollbarHolder) { |
|||
scrollbarHolder.width = `${this.sbw}px` |
|||
Object.assign(this.sBH, scrollbarHolder) |
|||
Object.assign(this.scrollBarHolder.style, scrollbarHolder) |
|||
} |
|||
|
|||
return this |
|||
if (scrollbarHolder) { |
|||
scrollbarHolder.width = `${this.sbw}px` |
|||
Object.assign(this.sBH, scrollbarHolder) |
|||
Object.assign(this.scrollBarHolder.style, scrollbarHolder) |
|||
} |
|||
|
|||
return this |
|||
} |
|||
} |
|||
|
|||
export default Scrollbot |
|||
|
@ -1,44 +1,45 @@ |
|||
*, |
|||
*::before, |
|||
*::after { |
|||
box-sizing: border-box; |
|||
margin: 0; |
|||
font-weight: normal; |
|||
box-sizing: border-box; |
|||
margin: 0; |
|||
font-weight: normal; |
|||
} |
|||
|
|||
html { |
|||
--text-normal: #6b6b6b; |
|||
--text-hover: #000000; |
|||
height: 100%; |
|||
--text-normal: #6b6b6b; |
|||
--text-hover: #000000; |
|||
height: 100%; |
|||
color: var(--text-normal); |
|||
} |
|||
|
|||
body { |
|||
--at-apply: text-normal; |
|||
height: 100%; |
|||
--at-apply: text-normal; |
|||
height: 100%; |
|||
} |
|||
|
|||
#app { |
|||
height: 100%; |
|||
height: 100%; |
|||
} |
|||
|
|||
* { |
|||
user-select: none; |
|||
outline: none; |
|||
user-select: none; |
|||
outline: none; |
|||
} |
|||
|
|||
.simplebar-scrollbar::before { |
|||
background-color: #bdbdbd; |
|||
border-radius: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
top: 0; |
|||
background-color: #bdbdbd; |
|||
border-radius: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
top: 0; |
|||
} |
|||
|
|||
.simplebar-hover .simplebar-scrollbar::before { |
|||
background-color: #909090; |
|||
background-color: #909090; |
|||
} |
|||
|
|||
.simplebar-wrapper:hover ~ .simplebar-track > .simplebar-scrollbar:before { |
|||
opacity: 0.5 !important; |
|||
opacity: 0.5 !important; |
|||
} |
|||
|
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue