├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── LICENSE ├── README.md ├── auto-imports.d.ts ├── build └── postBuildPlugin.ts ├── docs └── img1.png ├── index.html ├── logo.psd ├── package.json ├── pnpm-lock.yaml ├── public ├── logo.png ├── plugin.json └── preload.js ├── src ├── App.vue ├── common │ ├── directives.ts │ ├── icons │ │ ├── attachment.svg │ │ ├── command.svg │ │ ├── crosshairs-gps.svg │ │ ├── image.svg │ │ ├── index.ts │ │ ├── infinity.svg │ │ └── window.svg │ ├── monaco │ │ ├── addActions.ts │ │ ├── index.ts │ │ └── types │ │ │ ├── electron.d.ts │ │ │ ├── node.api.d.ts │ │ │ └── utools.api.d.ts │ ├── registerCallback.ts │ ├── registerIcon.ts │ └── symbol.ts ├── components │ ├── CodeList.vue │ ├── Console.vue │ ├── Editor.vue │ ├── SettingContent.vue │ └── ShortCut.vue ├── hooks │ ├── useEventBus.ts │ ├── useEventListener.ts │ ├── useMedia.ts │ ├── usePageBack.ts │ └── usePrefersTheme.ts ├── main.ts ├── preload │ └── index.ts ├── router │ └── index.ts ├── store │ ├── index.ts │ ├── useCodeStore.ts │ ├── useHistoryStore.ts │ ├── useScriptStore.ts │ └── useSettingStore.ts ├── style │ ├── border.less │ ├── index.less │ ├── scrollbar.less │ └── transition.less ├── utils │ ├── business.ts │ ├── env.ts │ ├── index.ts │ ├── storage.ts │ ├── tools.ts │ └── utools.ts ├── views │ ├── About.vue │ ├── Runner.vue │ └── Setting.vue └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | workflow_dispatch: 8 | 9 | jobs: 10 | BuildProject: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Use Node.js 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: '18' 19 | - name: Install pnpm 20 | uses: pnpm/action-setup@v2.2.2 21 | id: pnpm-install 22 | with: 23 | version: 8 24 | run_install: false 25 | - name: Install dependencies 26 | run: pnpm install 27 | - name: Build 28 | run: pnpm run build 29 | - name: Deploy Github Pages 30 | uses: JamesIves/github-pages-deploy-action@releases/v4 31 | with: 32 | folder: ./dist 33 | branch: gh-pages 34 | ssh-key: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | **/.DS_Store 4 | 5 | components.d.ts -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSRunner 2 | 3 | [⭐️ 主页 | WebSite](https://ziuchen.github.io/project/JSRunner/) 4 | [🕶️ 在线体验 | Online](https://ziuchen.github.io/JSRunner/) 5 | [🚚 更新日志 | Changelog](https://ziuchen.github.io/project/JSRunner/log/) 6 | 7 | - ✅ Run JavaScript code to quickly verify code logic 8 | - ✅ Support switching NodeJS/browser operating environment 9 | - ✅ `Ctrl/Command+R` Quickly run code 10 | - ✅ `Ctrl/Command+Q` Clear the console 11 | - ✅ `Ctrl/Command+N` Create new code snippet 12 | - ✅ `Ctrl/Command+E` Toggle Lock status 13 | - ✅ `Ctrl/Command+Shift+P` Call Command Palette 14 | - ✅ `Ctrl/Command+Shift+L` List all history 15 | - ✅ Support backtracking code history. Support saving/editing code running history 16 | - ✅ Support manually triggering code execution/running code in real time 17 | - ✅ Support top-level await. Adapt to dark mode 18 | 19 | ![](./docs/img1.png) -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-auto-import 5 | export {} 6 | declare global { 7 | const EffectScope: typeof import('vue')['EffectScope'] 8 | const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] 9 | const computed: typeof import('vue')['computed'] 10 | const createApp: typeof import('vue')['createApp'] 11 | const createPinia: typeof import('pinia')['createPinia'] 12 | const customRef: typeof import('vue')['customRef'] 13 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 14 | const defineComponent: typeof import('vue')['defineComponent'] 15 | const defineStore: typeof import('pinia')['defineStore'] 16 | const effectScope: typeof import('vue')['effectScope'] 17 | const getActivePinia: typeof import('pinia')['getActivePinia'] 18 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 19 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 20 | const h: typeof import('vue')['h'] 21 | const inject: typeof import('vue')['inject'] 22 | const isProxy: typeof import('vue')['isProxy'] 23 | const isReactive: typeof import('vue')['isReactive'] 24 | const isReadonly: typeof import('vue')['isReadonly'] 25 | const isRef: typeof import('vue')['isRef'] 26 | const mapActions: typeof import('pinia')['mapActions'] 27 | const mapGetters: typeof import('pinia')['mapGetters'] 28 | const mapState: typeof import('pinia')['mapState'] 29 | const mapStores: typeof import('pinia')['mapStores'] 30 | const mapWritableState: typeof import('pinia')['mapWritableState'] 31 | const markRaw: typeof import('vue')['markRaw'] 32 | const nextTick: typeof import('vue')['nextTick'] 33 | const onActivated: typeof import('vue')['onActivated'] 34 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 35 | const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] 36 | const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] 37 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 38 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 39 | const onDeactivated: typeof import('vue')['onDeactivated'] 40 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 41 | const onMounted: typeof import('vue')['onMounted'] 42 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 43 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 44 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 45 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 46 | const onUnmounted: typeof import('vue')['onUnmounted'] 47 | const onUpdated: typeof import('vue')['onUpdated'] 48 | const provide: typeof import('vue')['provide'] 49 | const reactive: typeof import('vue')['reactive'] 50 | const readonly: typeof import('vue')['readonly'] 51 | const ref: typeof import('vue')['ref'] 52 | const resolveComponent: typeof import('vue')['resolveComponent'] 53 | const setActivePinia: typeof import('pinia')['setActivePinia'] 54 | const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] 55 | const shallowReactive: typeof import('vue')['shallowReactive'] 56 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 57 | const shallowRef: typeof import('vue')['shallowRef'] 58 | const storeToRefs: typeof import('pinia')['storeToRefs'] 59 | const toRaw: typeof import('vue')['toRaw'] 60 | const toRef: typeof import('vue')['toRef'] 61 | const toRefs: typeof import('vue')['toRefs'] 62 | const triggerRef: typeof import('vue')['triggerRef'] 63 | const unref: typeof import('vue')['unref'] 64 | const useAttrs: typeof import('vue')['useAttrs'] 65 | const useCssModule: typeof import('vue')['useCssModule'] 66 | const useCssVars: typeof import('vue')['useCssVars'] 67 | const useLink: typeof import('vue-router')['useLink'] 68 | const useRoute: typeof import('vue-router')['useRoute'] 69 | const useRouter: typeof import('vue-router')['useRouter'] 70 | const useSlots: typeof import('vue')['useSlots'] 71 | const watch: typeof import('vue')['watch'] 72 | const watchEffect: typeof import('vue')['watchEffect'] 73 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 74 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 75 | } 76 | // for type re-export 77 | declare global { 78 | // @ts-ignore 79 | export type { Component,ComponentPublicInstance,ComputedRef,InjectionKey,PropType,Ref,VNode } from 'vue' 80 | } 81 | -------------------------------------------------------------------------------- /build/postBuildPlugin.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs' 3 | import { Plugin, transformWithEsbuild } from 'vite' 4 | import htmlMinifier from 'html-minifier-terser' 5 | 6 | export interface postBuildPluginOptions { 7 | /** 8 | * 需要处理的文件相对于dist目录的路径 9 | */ 10 | files: string[] 11 | } 12 | 13 | /** 14 | * Vite 构建后处理插件 15 | */ 16 | export default function postBuildPlugin(options: postBuildPluginOptions): Plugin { 17 | let config = null 18 | 19 | return { 20 | name: 'post-build-plugin', 21 | apply: 'build', 22 | 23 | configResolved(resolvedConfig) { 24 | config = resolvedConfig 25 | }, 26 | 27 | // 构建完成后 将构建后的dist目录下的文件进行最小化处理 28 | // *.html *.js *.svg *.json 29 | async closeBundle() { 30 | const files = options.files || ['index.html'] 31 | const distDir = config.build.outDir 32 | 33 | for (const file of files) { 34 | const fpath = path.resolve(distDir, file) 35 | const ext = path.extname(fpath) 36 | const execMap = { 37 | '.html': async (code: string) => 38 | htmlMinifier.minify(code, { 39 | removeComments: true, 40 | collapseWhitespace: true, 41 | collapseBooleanAttributes: true, 42 | removeAttributeQuotes: false, 43 | removeEmptyAttributes: true, 44 | minifyCSS: true, 45 | minifyJS: true, 46 | minifyURLs: true 47 | }), 48 | '.js': async (code: string) => { 49 | const res = await transformWithEsbuild(code, '', { 50 | loader: 'js', 51 | minify: true 52 | }) 53 | return res.code 54 | }, 55 | '.json': async (code: string) => JSON.stringify(JSON.parse(code)) 56 | } 57 | 58 | if (execMap[ext]) { 59 | const code = fs.readFileSync(fpath, 'utf-8') 60 | const codeTransformed = await execMap[ext](code) 61 | fs.writeFileSync(fpath, codeTransformed) 62 | } else { 63 | console.error(`Not Supported File Type: .${ext}`) 64 | } 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /docs/img1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZiuChen/JSRunner/ff9c5cd00a28c01e765e4d7bc7d07a31758b2a0a/docs/img1.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ZiuChen | 超级JavaScript 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZiuChen/JSRunner/ff9c5cd00a28c01e765e4d7bc7d07a31758b2a0a/logo.psd -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "vite", 4 | "build": "vue-tsc && vite build", 5 | "preview": "vite preview" 6 | }, 7 | "dependencies": { 8 | "@arco-design/web-vue": "^2.54.1", 9 | "@vueuse/core": "^10.7.1", 10 | "comment-parser": "^1.4.1", 11 | "lodash-es": "^4.17.21", 12 | "monaco-editor": "^0.37.1", 13 | "pinia": "^2.1.7", 14 | "tiny-emitter": "^2.1.0", 15 | "vue": "^3.4.5", 16 | "vue-router": "^4.2.5" 17 | }, 18 | "devDependencies": { 19 | "@types/html-minifier-terser": "^7.0.2", 20 | "@types/lodash-es": "^4.17.12", 21 | "@types/node": "^20.10.6", 22 | "@vitejs/plugin-vue": "^5.0.2", 23 | "html-minifier-terser": "^7.2.0", 24 | "less": "^4.2.0", 25 | "typescript": "^4.9.5", 26 | "unplugin-auto-import": "^0.14.4", 27 | "unplugin-vue-components": "^0.24.1", 28 | "utools-api-types": "^3.0.0", 29 | "vite": "^5.0.10", 30 | "vite-plugin-style-import": "^2.0.0", 31 | "vue-tsc": "^1.8.27" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZiuChen/JSRunner/ff9c5cd00a28c01e765e4d7bc7d07a31758b2a0a/public/logo.png -------------------------------------------------------------------------------- /public/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginName": "超级JavaScript", 3 | "description": "JavaScript运行器 支持多种运行环境", 4 | "author": "ZiuChen", 5 | "homepage": "https://github.com/ZiuChen", 6 | "main": "index.html", 7 | "preload": "preload.js", 8 | "development": { 9 | "main": "http://localhost:8084/" 10 | }, 11 | "logo": "logo.png", 12 | "platform": ["win32", "darwin", "linux"], 13 | "features": [ 14 | { 15 | "code": "超级JavaScript", 16 | "icon": "logo.png", 17 | "explain": "进入 JavaScript 运行器", 18 | "cmds": ["JavaScript Runner", "Super JavaScript", "Run Code", "运行代码", "超级JavaScript"] 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /public/preload.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron') 2 | const { Buffer } = require('buffer') 3 | const vm = require('vm') 4 | 5 | window.preload = { 6 | electron, 7 | Buffer, 8 | require, 9 | vm 10 | } 11 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 24 | -------------------------------------------------------------------------------- /src/common/directives.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue' 2 | 3 | export function registerDirectives(app: App) { 4 | // register a directive call v-zoom 5 | // when v-zoom is true, the element scale to 1.2 and quickly back to 1.0 6 | app.directive('zoom', (el, binding) => { 7 | if (binding.value) { 8 | el.style.transform = 'scale(1.6)' 9 | el.style.transition = 'transform 0.2s' 10 | setTimeout(() => { 11 | el.style.transform = 'scale(1.0)' 12 | }, 200) 13 | } 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /src/common/icons/attachment.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/common/icons/command.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/common/icons/crosshairs-gps.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/common/icons/image.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/common/icons/index.ts: -------------------------------------------------------------------------------- 1 | import attachment from './attachment.svg' 2 | import command from './command.svg' 3 | import crosshairsGps from './crosshairs-gps.svg' 4 | import image from './image.svg' 5 | import infinity from './infinity.svg' 6 | import window from './window.svg' 7 | 8 | // Wrap the icons as Vue components 9 | export const AttachmentIcon = wrapIcon(attachment, 'IconAttachment') 10 | export const CommandIcon = wrapIcon(command, 'IconCommand') 11 | export const CrosshairsGpsIcon = wrapIcon(crosshairsGps, 'IconCrosshairsGps') 12 | export const ImageIcon = wrapIcon(image, 'IconImage') 13 | export const InfinityIcon = wrapIcon(infinity, 'IconInfinity') 14 | export const WindowIcon = wrapIcon(window, 'IconWindow') 15 | 16 | function wrapIcon(svg: string, name: string) { 17 | return () => h(name, [h('svg', { innerHTML: svg })]) 18 | } 19 | -------------------------------------------------------------------------------- /src/common/icons/infinity.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/common/icons/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/common/monaco/addActions.ts: -------------------------------------------------------------------------------- 1 | import type { defineEmits } from 'vue' 2 | import { monaco } from '@/common/monaco' 3 | // @ts-expect-error 4 | import { IQuickInputService } from 'monaco-editor/esm/vs/platform/quickinput/common/quickInput' 5 | import { CodeHistory } from '@/store/useHistoryStore' 6 | import { useCodeStore } from '@/store' 7 | 8 | /** 9 | * 注册自定义菜单项 10 | * 并透传到组件 11 | */ 12 | export function addActions( 13 | editor: monaco.editor.IStandaloneCodeEditor, 14 | emit: ReturnType 15 | ) { 16 | // Add a new command, for getting an accessor. 17 | const quickInputCommand = editor.addCommand(0, (accessor, func) => { 18 | // a hacker way to get the input service 19 | const quickInputService = accessor.get(IQuickInputService) 20 | func(quickInputService) 21 | }) 22 | 23 | editor.addAction({ 24 | id: 'quickInput', 25 | label: 'Quick Input', 26 | contextMenuGroupId: 'buildin-action', 27 | run: (editor, { list, callback, options }) => { 28 | editor.trigger('', quickInputCommand!, (quickInput: any) => { 29 | quickInput.pick(list, options).then((selected?: any) => { 30 | callback(selected) 31 | }) 32 | }) 33 | } 34 | }) 35 | 36 | editor.addAction({ 37 | id: 'run', // 菜单项 id 38 | label: 'Run', // 菜单项名称 39 | keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyR], 40 | contextMenuGroupId: 'buildin-action', // 所属菜单的分组 41 | run: () => emit('action', 'runCode') // 菜单项点击事件 42 | }) 43 | 44 | editor.addAction({ 45 | id: 'newCode', 46 | label: 'New Code', 47 | keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyN], 48 | contextMenuGroupId: 'buildin-action', 49 | run: () => emit('action', 'newCode') 50 | }) 51 | 52 | editor.addAction({ 53 | id: 'clearConsole', 54 | label: 'Clear Console', 55 | keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyQ], 56 | contextMenuGroupId: 'buildin-action', 57 | run: () => emit('action', 'clearConsole') 58 | }) 59 | 60 | editor.addAction({ 61 | id: 'toggleReadonly', 62 | label: 'Toggle Readonly', 63 | keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyE], 64 | contextMenuGroupId: 'buildin-action', 65 | run: () => emit('action', 'toggleReadonly') 66 | }) 67 | 68 | editor.addAction({ 69 | id: 'showCommands', 70 | label: 'Show Commands', 71 | keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyP], 72 | contextMenuGroupId: 'buildin-action', 73 | run: () => emit('action', 'showCommands') 74 | }) 75 | 76 | editor.addAction({ 77 | id: 'listHistory', 78 | label: 'List History', 79 | keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyL], 80 | contextMenuGroupId: 'buildin-action', 81 | run: () => { 82 | emit('action', 'listHistory') 83 | } 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /src/common/monaco/index.ts: -------------------------------------------------------------------------------- 1 | // accessibilityHelp 2 | import 'monaco-editor/esm/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp' 3 | // anchorSelect 4 | import 'monaco-editor/esm/vs/editor/contrib/anchorSelect/browser/anchorSelect' 5 | // bracketMatching 6 | import 'monaco-editor/esm/vs/editor/contrib/bracketMatching/browser/bracketMatching' 7 | // browser 8 | import 'monaco-editor/esm/vs/editor/browser/coreCommands' 9 | // caretOperations 10 | import 'monaco-editor/esm/vs/editor/contrib/caretOperations/browser/caretOperations' 11 | import 'monaco-editor/esm/vs/editor/contrib/caretOperations/browser/transpose' 12 | // clipboard 13 | import 'monaco-editor/esm/vs/editor/contrib/clipboard/browser/clipboard' 14 | // codeAction 15 | import 'monaco-editor/esm/vs/editor/contrib/codeAction/browser/codeActionContributions' 16 | // codelens 17 | import 'monaco-editor/esm/vs/editor/contrib/codelens/browser/codelensController' 18 | // colorPicker 19 | import 'monaco-editor/esm/vs/editor/contrib/colorPicker/browser/colorContributions' 20 | // comment 21 | import 'monaco-editor/esm/vs/editor/contrib/comment/browser/comment' 22 | // contextmenu 23 | import 'monaco-editor/esm/vs/editor/contrib/contextmenu/browser/contextmenu' 24 | // copyPaste 25 | import 'monaco-editor/esm/vs/editor/contrib/copyPaste/browser/copyPasteContribution' 26 | // cursorUndo 27 | import 'monaco-editor/esm/vs/editor/contrib/cursorUndo/browser/cursorUndo' 28 | // dnd 29 | import 'monaco-editor/esm/vs/editor/contrib/dnd/browser/dnd' 30 | // documentSymbols 31 | import 'monaco-editor/esm/vs/editor/contrib/documentSymbols/browser/documentSymbols' 32 | // dropIntoEditor 33 | import 'monaco-editor/esm/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution' 34 | // find 35 | import 'monaco-editor/esm/vs/editor/contrib/find/browser/findController' 36 | // folding 37 | import 'monaco-editor/esm/vs/editor/contrib/folding/browser/folding' 38 | // fontZoom 39 | import 'monaco-editor/esm/vs/editor/contrib/fontZoom/browser/fontZoom' 40 | // format 41 | import 'monaco-editor/esm/vs/editor/contrib/format/browser/formatActions' 42 | // gotoError 43 | import 'monaco-editor/esm/vs/editor/contrib/gotoError/browser/gotoError' 44 | // gotoLine 45 | import 'monaco-editor/esm/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess' 46 | // gotoSymbol 47 | import 'monaco-editor/esm/vs/editor/contrib/gotoSymbol/browser/goToCommands' 48 | import 'monaco-editor/esm/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition' 49 | // hover 50 | import 'monaco-editor/esm/vs/editor/contrib/hover/browser/hover' 51 | // iPadShowKeyboard 52 | import 'monaco-editor/esm/vs/editor/standalone/browser/iPadShowKeyboard/iPadShowKeyboard' 53 | // inPlaceReplace 54 | import 'monaco-editor/esm/vs/editor/contrib/inPlaceReplace/browser/inPlaceReplace' 55 | // indentation 56 | import 'monaco-editor/esm/vs/editor/contrib/indentation/browser/indentation' 57 | // inlayHints 58 | import 'monaco-editor/esm/vs/editor/contrib/inlayHints/browser/inlayHintsContribution' 59 | // inlineCompletions 60 | import 'monaco-editor/esm/vs/editor/contrib/inlineCompletions/browser/ghostText.contribution' 61 | // inspectTokens 62 | import 'monaco-editor/esm/vs/editor/standalone/browser/inspectTokens/inspectTokens' 63 | // lineSelection 64 | import 'monaco-editor/esm/vs/editor/contrib/lineSelection/browser/lineSelection' 65 | // linesOperations 66 | import 'monaco-editor/esm/vs/editor/contrib/linesOperations/browser/linesOperations' 67 | // linkedEditing 68 | import 'monaco-editor/esm/vs/editor/contrib/linkedEditing/browser/linkedEditing' 69 | // links 70 | import 'monaco-editor/esm/vs/editor/contrib/links/browser/links' 71 | // longLinesHelper 72 | import 'monaco-editor/esm/vs/editor/contrib/longLinesHelper/browser/longLinesHelper' 73 | // multicursor 74 | import 'monaco-editor/esm/vs/editor/contrib/multicursor/browser/multicursor' 75 | // parameterHints 76 | import 'monaco-editor/esm/vs/editor/contrib/parameterHints/browser/parameterHints' 77 | // quickCommand 78 | import 'monaco-editor/esm/vs/editor/standalone/browser/quickAccess/standaloneCommandsQuickAccess' 79 | // quickHelp 80 | import 'monaco-editor/esm/vs/editor/standalone/browser/quickAccess/standaloneHelpQuickAccess' 81 | // quickOutline 82 | import 'monaco-editor/esm/vs/editor/standalone/browser/quickAccess/standaloneGotoSymbolQuickAccess' 83 | // readOnlyMessage 84 | import 'monaco-editor/esm/vs/editor/contrib/readOnlyMessage/browser/contribution' 85 | // referenceSearch 86 | import 'monaco-editor/esm/vs/editor/standalone/browser/referenceSearch/standaloneReferenceSearch' 87 | // rename 88 | import 'monaco-editor/esm/vs/editor/contrib/rename/browser/rename' 89 | // semanticTokens 90 | import 'monaco-editor/esm/vs/editor/contrib/semanticTokens/browser/documentSemanticTokens' 91 | import 'monaco-editor/esm/vs/editor/contrib/semanticTokens/browser/viewportSemanticTokens' 92 | // smartSelect 93 | import 'monaco-editor/esm/vs/editor/contrib/smartSelect/browser/smartSelect' 94 | // snippet 95 | import 'monaco-editor/esm/vs/editor/contrib/snippet/browser/snippetController2' 96 | // stickyScroll 97 | import 'monaco-editor/esm/vs/editor/contrib/stickyScroll/browser/stickyScrollContribution' 98 | // suggest 99 | import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestController' 100 | import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestInlineCompletions' 101 | // toggleHighContrast 102 | import 'monaco-editor/esm/vs/editor/standalone/browser/toggleHighContrast/toggleHighContrast' 103 | // toggleTabFocusMode 104 | import 'monaco-editor/esm/vs/editor/contrib/toggleTabFocusMode/browser/toggleTabFocusMode' 105 | // tokenization 106 | import 'monaco-editor/esm/vs/editor/contrib/tokenization/browser/tokenization' 107 | // unicodeHighlighter 108 | import 'monaco-editor/esm/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter' 109 | // unusualLineTerminators 110 | import 'monaco-editor/esm/vs/editor/contrib/unusualLineTerminators/browser/unusualLineTerminators' 111 | // wordHighlighter 112 | import 'monaco-editor/esm/vs/editor/contrib/wordHighlighter/browser/wordHighlighter' 113 | // wordOperations 114 | import 'monaco-editor/esm/vs/editor/contrib/wordOperations/browser/wordOperations' 115 | // wordPartOperations 116 | import 'monaco-editor/esm/vs/editor/contrib/wordPartOperations/browser/wordPartOperations' 117 | 118 | import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker' 119 | import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution' 120 | import 'monaco-editor/esm/vs/basic-languages/typescript/typescript.contribution' 121 | import 'monaco-editor/esm/vs/language/typescript/monaco.contribution' 122 | import TSWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker' 123 | import JSONWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker' 124 | import 'monaco-editor/esm/vs/language/json/monaco.contribution' 125 | 126 | import * as monaco from 'monaco-editor/esm/vs/editor/editor.api' 127 | 128 | self.MonacoEnvironment = { 129 | getWorker(_: string, label: string) { 130 | if (label === 'typescript' || label === 'javascript') { 131 | return new TSWorker() 132 | } 133 | if (label === 'json') { 134 | return new JSONWorker() 135 | } 136 | return new EditorWorker() 137 | } 138 | } 139 | 140 | // 导入声明文件 141 | import uToolsApis from './types/utools.api.d.ts?raw' 142 | import NodeApis from './types/node.api.d.ts?raw' 143 | import ElectronApis from './types/electron.d.ts?raw' 144 | 145 | monaco.languages.typescript.javascriptDefaults.addExtraLib(uToolsApis, 'utools.api.d.ts') 146 | monaco.languages.typescript.javascriptDefaults.addExtraLib(NodeApis, 'node.api.d.ts') 147 | monaco.languages.typescript.javascriptDefaults.addExtraLib(ElectronApis, 'electron.api.d.ts') 148 | 149 | monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ 150 | noSemanticValidation: true, 151 | noSyntaxValidation: false 152 | }) 153 | 154 | monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ 155 | target: monaco.languages.typescript.ScriptTarget.ES2016, 156 | allowNonTsExtensions: true, 157 | allowJs: true 158 | }) 159 | 160 | export { monaco } 161 | -------------------------------------------------------------------------------- /src/common/monaco/types/electron.d.ts: -------------------------------------------------------------------------------- 1 | interface Clipboard { 2 | 3 | // Docs: https://electronjs.org/docs/api/clipboard 4 | 5 | /** 6 | * An array of supported formats for the clipboard `type`. 7 | */ 8 | availableFormats(type?: 'selection' | 'clipboard'): string[]; 9 | /** 10 | * Clears the clipboard content. 11 | */ 12 | clear(type?: 'selection' | 'clipboard'): void; 13 | /** 14 | * Whether the clipboard supports the specified `format`. 15 | * 16 | * @experimental 17 | */ 18 | has(format: string, type?: 'selection' | 'clipboard'): boolean; 19 | /** 20 | * Reads `format` type from the clipboard. 21 | * 22 | * `format` should contain valid ASCII characters and have `/` separator. `a/c`, 23 | * `a/bc` are valid formats while `/abc`, `abc/`, `a/`, `/a`, `a` are not valid. 24 | * 25 | * @experimental 26 | */ 27 | read(format: string): string; 28 | /** 29 | * * `title` string 30 | * * `url` string 31 | * 32 | * Returns an Object containing `title` and `url` keys representing the bookmark in 33 | * the clipboard. The `title` and `url` values will be empty strings when the 34 | * bookmark is unavailable. The `title` value will always be empty on Windows. 35 | * 36 | * @platform darwin,win32 37 | */ 38 | readBookmark(): ReadBookmark; 39 | /** 40 | * Reads `format` type from the clipboard. 41 | * 42 | * @experimental 43 | */ 44 | readBuffer(format: string): Buffer; 45 | /** 46 | * The text on the find pasteboard, which is the pasteboard that holds information 47 | * about the current state of the active application’s find panel. 48 | * 49 | * This method uses synchronous IPC when called from the renderer process. The 50 | * cached value is reread from the find pasteboard whenever the application is 51 | * activated. 52 | * 53 | * @platform darwin 54 | */ 55 | readFindText(): string; 56 | /** 57 | * The content in the clipboard as markup. 58 | */ 59 | readHTML(type?: 'selection' | 'clipboard'): string; 60 | /** 61 | * The image content in the clipboard. 62 | */ 63 | readImage(type?: 'selection' | 'clipboard'): NativeImage; 64 | /** 65 | * The content in the clipboard as RTF. 66 | */ 67 | readRTF(type?: 'selection' | 'clipboard'): string; 68 | /** 69 | * The content in the clipboard as plain text. 70 | */ 71 | readText(type?: 'selection' | 'clipboard'): string; 72 | /** 73 | * Writes `data` to the clipboard. 74 | */ 75 | write(data: Data, type?: 'selection' | 'clipboard'): void; 76 | /** 77 | * Writes the `title` (macOS only) and `url` into the clipboard as a bookmark. 78 | * 79 | * **Note:** Most apps on Windows don't support pasting bookmarks into them so you 80 | * can use `clipboard.write` to write both a bookmark and fallback text to the 81 | * clipboard. 82 | * 83 | * @platform darwin,win32 84 | */ 85 | writeBookmark(title: string, url: string, type?: 'selection' | 'clipboard'): void; 86 | /** 87 | * Writes the `buffer` into the clipboard as `format`. 88 | * 89 | * @experimental 90 | */ 91 | writeBuffer(format: string, buffer: Buffer, type?: 'selection' | 'clipboard'): void; 92 | /** 93 | * Writes the `text` into the find pasteboard (the pasteboard that holds 94 | * information about the current state of the active application’s find panel) as 95 | * plain text. This method uses synchronous IPC when called from the renderer 96 | * process. 97 | * 98 | * @platform darwin 99 | */ 100 | writeFindText(text: string): void; 101 | /** 102 | * Writes `markup` to the clipboard. 103 | */ 104 | writeHTML(markup: string, type?: 'selection' | 'clipboard'): void; 105 | /** 106 | * Writes `image` to the clipboard. 107 | */ 108 | writeImage(image: NativeImage, type?: 'selection' | 'clipboard'): void; 109 | /** 110 | * Writes the `text` into the clipboard in RTF. 111 | */ 112 | writeRTF(text: string, type?: 'selection' | 'clipboard'): void; 113 | /** 114 | * Writes the `text` into the clipboard as plain text. 115 | */ 116 | writeText(text: string, type?: 'selection' | 'clipboard'): void; 117 | } 118 | interface ContextBridge { 119 | 120 | // Docs: https://electronjs.org/docs/api/context-bridge 121 | 122 | exposeInMainWorld(apiKey: string, api: any): void; 123 | } 124 | interface CrashReporter { 125 | 126 | // Docs: https://electronjs.org/docs/api/crash-reporter 127 | 128 | /** 129 | * Set an extra parameter to be sent with the crash report. The values specified 130 | * here will be sent in addition to any values set via the `extra` option when 131 | * `start` was called. 132 | * 133 | * Parameters added in this fashion (or via the `extra` parameter to 134 | * `crashReporter.start`) are specific to the calling process. Adding extra 135 | * parameters in the main process will not cause those parameters to be sent along 136 | * with crashes from renderer or other child processes. Similarly, adding extra 137 | * parameters in a renderer process will not result in those parameters being sent 138 | * with crashes that occur in other renderer processes or in the main process. 139 | * 140 | * **Note:** Parameters have limits on the length of the keys and values. Key names 141 | * must be no longer than 39 bytes, and values must be no longer than 20320 bytes. 142 | * Keys with names longer than the maximum will be silently ignored. Key values 143 | * longer than the maximum length will be truncated. 144 | */ 145 | addExtraParameter(key: string, value: string): void; 146 | /** 147 | * The date and ID of the last crash report. Only crash reports that have been 148 | * uploaded will be returned; even if a crash report is present on disk it will not 149 | * be returned until it is uploaded. In the case that there are no uploaded 150 | * reports, `null` is returned. 151 | * 152 | * **Note:** This method is only available in the main process. 153 | */ 154 | getLastCrashReport(): CrashReport; 155 | /** 156 | * The current 'extra' parameters of the crash reporter. 157 | */ 158 | getParameters(): Record; 159 | /** 160 | * Returns all uploaded crash reports. Each report contains the date and uploaded 161 | * ID. 162 | * 163 | * **Note:** This method is only available in the main process. 164 | */ 165 | getUploadedReports(): CrashReport[]; 166 | /** 167 | * Whether reports should be submitted to the server. Set through the `start` 168 | * method or `setUploadToServer`. 169 | * 170 | * **Note:** This method is only available in the main process. 171 | */ 172 | getUploadToServer(): boolean; 173 | /** 174 | * Remove an extra parameter from the current set of parameters. Future crashes 175 | * will not include this parameter. 176 | */ 177 | removeExtraParameter(key: string): void; 178 | /** 179 | * This would normally be controlled by user preferences. This has no effect if 180 | * called before `start` is called. 181 | * 182 | * **Note:** This method is only available in the main process. 183 | */ 184 | setUploadToServer(uploadToServer: boolean): void; 185 | /** 186 | * This method must be called before using any other `crashReporter` APIs. Once 187 | * initialized this way, the crashpad handler collects crashes from all 188 | * subsequently created processes. The crash reporter cannot be disabled once 189 | * started. 190 | * 191 | * This method should be called as early as possible in app startup, preferably 192 | * before `app.on('ready')`. If the crash reporter is not initialized at the time a 193 | * renderer process is created, then that renderer process will not be monitored by 194 | * the crash reporter. 195 | * 196 | * **Note:** You can test out the crash reporter by generating a crash using 197 | * `process.crash()`. 198 | * 199 | * **Note:** If you need to send additional/updated `extra` parameters after your 200 | * first call `start` you can call `addExtraParameter`. 201 | * 202 | * **Note:** Parameters passed in `extra`, `globalExtra` or set with 203 | * `addExtraParameter` have limits on the length of the keys and values. Key names 204 | * must be at most 39 bytes long, and values must be no longer than 127 bytes. Keys 205 | * with names longer than the maximum will be silently ignored. Key values longer 206 | * than the maximum length will be truncated. 207 | * 208 | * **Note:** This method is only available in the main process. 209 | */ 210 | start(options: CrashReporterStartOptions): void; 211 | } 212 | interface IpcRenderer extends NodeJS.EventEmitter { 213 | 214 | // Docs: https://electronjs.org/docs/api/ipc-renderer 215 | 216 | /** 217 | * Resolves with the response from the main process. 218 | * 219 | * Send a message to the main process via `channel` and expect a result 220 | * asynchronously. Arguments will be serialized with the Structured Clone 221 | * Algorithm, just like `window.postMessage`, so prototype chains will not be 222 | * included. Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will throw 223 | * an exception. 224 | * 225 | * > **NOTE:** Sending non-standard JavaScript types such as DOM objects or special 226 | * Electron objects will throw an exception. 227 | * 228 | * Since the main process does not have support for DOM objects such as 229 | * `ImageBitmap`, `File`, `DOMMatrix` and so on, such objects cannot be sent over 230 | * Electron's IPC to the main process, as the main process would have no way to 231 | * decode them. Attempting to send such objects over IPC will result in an error. 232 | * 233 | * The main process should listen for `channel` with `ipcMain.handle()`. 234 | * 235 | * For example: 236 | * 237 | * If you need to transfer a `MessagePort` to the main process, use 238 | * `ipcRenderer.postMessage`. 239 | * 240 | * If you do not need a response to the message, consider using `ipcRenderer.send`. 241 | */ 242 | invoke(channel: string, ...args: any[]): Promise; 243 | /** 244 | * Listens to `channel`, when a new message arrives `listener` would be called with 245 | * `listener(event, args...)`. 246 | */ 247 | on(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): this; 248 | /** 249 | * Adds a one time `listener` function for the event. This `listener` is invoked 250 | * only the next time a message is sent to `channel`, after which it is removed. 251 | */ 252 | once(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): this; 253 | /** 254 | * Send a message to the main process, optionally transferring ownership of zero or 255 | * more `MessagePort` objects. 256 | * 257 | * The transferred `MessagePort` objects will be available in the main process as 258 | * `MessagePortMain` objects by accessing the `ports` property of the emitted 259 | * event. 260 | * 261 | * For example: 262 | * 263 | * For more information on using `MessagePort` and `MessageChannel`, see the MDN 264 | * documentation. 265 | */ 266 | postMessage(channel: string, message: any, transfer?: MessagePort[]): void; 267 | /** 268 | * Removes all listeners, or those of the specified `channel`. 269 | */ 270 | removeAllListeners(channel: string): this; 271 | /** 272 | * Removes the specified `listener` from the listener array for the specified 273 | * `channel`. 274 | */ 275 | removeListener(channel: string, listener: (...args: any[]) => void): this; 276 | /** 277 | * Send an asynchronous message to the main process via `channel`, along with 278 | * arguments. Arguments will be serialized with the Structured Clone Algorithm, 279 | * just like `window.postMessage`, so prototype chains will not be included. 280 | * Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will throw an 281 | * exception. 282 | * 283 | * > **NOTE:** Sending non-standard JavaScript types such as DOM objects or special 284 | * Electron objects will throw an exception. 285 | * 286 | * Since the main process does not have support for DOM objects such as 287 | * `ImageBitmap`, `File`, `DOMMatrix` and so on, such objects cannot be sent over 288 | * Electron's IPC to the main process, as the main process would have no way to 289 | * decode them. Attempting to send such objects over IPC will result in an error. 290 | * 291 | * The main process handles it by listening for `channel` with the `ipcMain` 292 | * module. 293 | * 294 | * If you need to transfer a `MessagePort` to the main process, use 295 | * `ipcRenderer.postMessage`. 296 | * 297 | * If you want to receive a single response from the main process, like the result 298 | * of a method call, consider using `ipcRenderer.invoke`. 299 | */ 300 | send(channel: string, ...args: any[]): void; 301 | /** 302 | * The value sent back by the `ipcMain` handler. 303 | * 304 | * Send a message to the main process via `channel` and expect a result 305 | * synchronously. Arguments will be serialized with the Structured Clone Algorithm, 306 | * just like `window.postMessage`, so prototype chains will not be included. 307 | * Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will throw an 308 | * exception. 309 | * 310 | * > **NOTE:** Sending non-standard JavaScript types such as DOM objects or special 311 | * Electron objects will throw an exception. 312 | * 313 | * Since the main process does not have support for DOM objects such as 314 | * `ImageBitmap`, `File`, `DOMMatrix` and so on, such objects cannot be sent over 315 | * Electron's IPC to the main process, as the main process would have no way to 316 | * decode them. Attempting to send such objects over IPC will result in an error. 317 | * 318 | * The main process handles it by listening for `channel` with `ipcMain` module, 319 | * and replies by setting `event.returnValue`. 320 | * 321 | * > :warning: **WARNING**: Sending a synchronous message will block the whole 322 | * renderer process until the reply is received, so use this method only as a last 323 | * resort. It's much better to use the asynchronous version, `invoke()`. 324 | */ 325 | sendSync(channel: string, ...args: any[]): any; 326 | /** 327 | * Sends a message to a window with `webContentsId` via `channel`. 328 | */ 329 | sendTo(webContentsId: number, channel: string, ...args: any[]): void; 330 | /** 331 | * Like `ipcRenderer.send` but the event will be sent to the `` element in 332 | * the host page instead of the main process. 333 | */ 334 | sendToHost(channel: string, ...args: any[]): void; 335 | } 336 | 337 | interface IpcRendererEvent extends Event { 338 | 339 | // Docs: https://electronjs.org/docs/api/structures/ipc-renderer-event 340 | 341 | /** 342 | * A list of MessagePorts that were transferred with this message 343 | */ 344 | ports: MessagePort[]; 345 | /** 346 | * The `IpcRenderer` instance that emitted the event originally 347 | */ 348 | sender: IpcRenderer; 349 | /** 350 | * The `webContents.id` that sent the message, you can call 351 | * `event.sender.sendTo(event.senderId, ...)` to reply to the message, see 352 | * ipcRenderer.sendTo for more information. This only applies to messages sent from 353 | * a different renderer. Messages sent directly from the main process set 354 | * `event.senderId` to `0`. 355 | */ 356 | senderId: number; 357 | } 358 | class NativeImage { 359 | 360 | // Docs: https://electronjs.org/docs/api/native-image 361 | 362 | /** 363 | * Creates an empty `NativeImage` instance. 364 | */ 365 | static createEmpty(): NativeImage; 366 | /** 367 | * Creates a new `NativeImage` instance from `buffer` that contains the raw bitmap 368 | * pixel data returned by `toBitmap()`. The specific format is platform-dependent. 369 | */ 370 | static createFromBitmap(buffer: Buffer, options: CreateFromBitmapOptions): NativeImage; 371 | /** 372 | * Creates a new `NativeImage` instance from `buffer`. Tries to decode as PNG or 373 | * JPEG first. 374 | */ 375 | static createFromBuffer(buffer: Buffer, options?: CreateFromBufferOptions): NativeImage; 376 | /** 377 | * Creates a new `NativeImage` instance from `dataURL`. 378 | */ 379 | static createFromDataURL(dataURL: string): NativeImage; 380 | /** 381 | * Creates a new `NativeImage` instance from the NSImage that maps to the given 382 | * image name. See `System Icons` for a list of possible values. 383 | * 384 | * The `hslShift` is applied to the image with the following rules: 385 | * 386 | * * `hsl_shift[0]` (hue): The absolute hue value for the image - 0 and 1 map to 0 387 | * and 360 on the hue color wheel (red). 388 | * * `hsl_shift[1]` (saturation): A saturation shift for the image, with the 389 | * following key values: 0 = remove all color. 0.5 = leave unchanged. 1 = fully 390 | * saturate the image. 391 | * * `hsl_shift[2]` (lightness): A lightness shift for the image, with the 392 | * following key values: 0 = remove all lightness (make all pixels black). 0.5 = 393 | * leave unchanged. 1 = full lightness (make all pixels white). 394 | * 395 | * This means that `[-1, 0, 1]` will make the image completely white and `[-1, 1, 396 | * 0]` will make the image completely black. 397 | * 398 | * In some cases, the `NSImageName` doesn't match its string representation; one 399 | * example of this is `NSFolderImageName`, whose string representation would 400 | * actually be `NSFolder`. Therefore, you'll need to determine the correct string 401 | * representation for your image before passing it in. This can be done with the 402 | * following: 403 | * 404 | * `echo -e '#import \nint main() { NSLog(@"%@", SYSTEM_IMAGE_NAME); 405 | * }' | clang -otest -x objective-c -framework Cocoa - && ./test` 406 | * 407 | * where `SYSTEM_IMAGE_NAME` should be replaced with any value from this list. 408 | * 409 | * @platform darwin 410 | */ 411 | static createFromNamedImage(imageName: string, hslShift?: number[]): NativeImage; 412 | /** 413 | * Creates a new `NativeImage` instance from a file located at `path`. This method 414 | * returns an empty image if the `path` does not exist, cannot be read, or is not a 415 | * valid image. 416 | */ 417 | static createFromPath(path: string): NativeImage; 418 | /** 419 | * fulfilled with the file's thumbnail preview image, which is a NativeImage. 420 | * 421 | * @platform darwin,win32 422 | */ 423 | static createThumbnailFromPath(path: string, maxSize: Size): Promise; 424 | /** 425 | * Add an image representation for a specific scale factor. This can be used to 426 | * explicitly add different scale factor representations to an image. This can be 427 | * called on empty images. 428 | */ 429 | addRepresentation(options: AddRepresentationOptions): void; 430 | /** 431 | * The cropped image. 432 | */ 433 | crop(rect: Rectangle): NativeImage; 434 | /** 435 | * The image's aspect ratio. 436 | * 437 | * If `scaleFactor` is passed, this will return the aspect ratio corresponding to 438 | * the image representation most closely matching the passed value. 439 | */ 440 | getAspectRatio(scaleFactor?: number): number; 441 | /** 442 | * A Buffer that contains the image's raw bitmap pixel data. 443 | * 444 | * The difference between `getBitmap()` and `toBitmap()` is that `getBitmap()` does 445 | * not copy the bitmap data, so you have to use the returned Buffer immediately in 446 | * current event loop tick; otherwise the data might be changed or destroyed. 447 | */ 448 | getBitmap(options?: BitmapOptions): Buffer; 449 | /** 450 | * A Buffer that stores C pointer to underlying native handle of the image. On 451 | * macOS, a pointer to `NSImage` instance would be returned. 452 | * 453 | * Notice that the returned pointer is a weak pointer to the underlying native 454 | * image instead of a copy, so you _must_ ensure that the associated `nativeImage` 455 | * instance is kept around. 456 | * 457 | * @platform darwin 458 | */ 459 | getNativeHandle(): Buffer; 460 | /** 461 | * An array of all scale factors corresponding to representations for a given 462 | * nativeImage. 463 | */ 464 | getScaleFactors(): number[]; 465 | /** 466 | * If `scaleFactor` is passed, this will return the size corresponding to the image 467 | * representation most closely matching the passed value. 468 | */ 469 | getSize(scaleFactor?: number): Size; 470 | /** 471 | * Whether the image is empty. 472 | */ 473 | isEmpty(): boolean; 474 | /** 475 | * Whether the image is a template image. 476 | */ 477 | isTemplateImage(): boolean; 478 | /** 479 | * The resized image. 480 | * 481 | * If only the `height` or the `width` are specified then the current aspect ratio 482 | * will be preserved in the resized image. 483 | */ 484 | resize(options: ResizeOptions): NativeImage; 485 | /** 486 | * Marks the image as a template image. 487 | */ 488 | setTemplateImage(option: boolean): void; 489 | /** 490 | * A Buffer that contains a copy of the image's raw bitmap pixel data. 491 | */ 492 | toBitmap(options?: ToBitmapOptions): Buffer; 493 | /** 494 | * The data URL of the image. 495 | */ 496 | toDataURL(options?: ToDataURLOptions): string; 497 | /** 498 | * A Buffer that contains the image's `JPEG` encoded data. 499 | */ 500 | toJPEG(quality: number): Buffer; 501 | /** 502 | * A Buffer that contains the image's `PNG` encoded data. 503 | */ 504 | toPNG(options?: ToPNGOptions): Buffer; 505 | /** 506 | * A `boolean` property that determines whether the image is considered a template 507 | * image. 508 | * 509 | * Please note that this property only has an effect on macOS. 510 | * 511 | * @platform darwin 512 | */ 513 | isMacTemplateImage: boolean; 514 | } 515 | 516 | interface Shell { 517 | 518 | // Docs: https://electronjs.org/docs/api/shell 519 | 520 | /** 521 | * Play the beep sound. 522 | */ 523 | beep(): void; 524 | /** 525 | * Open the given external protocol URL in the desktop's default manner. (For 526 | * example, mailto: URLs in the user's default mail agent). 527 | */ 528 | openExternal(url: string, options?: OpenExternalOptions): Promise; 529 | /** 530 | * Resolves with a string containing the error message corresponding to the failure 531 | * if a failure occurred, otherwise "". 532 | * 533 | * Open the given file in the desktop's default manner. 534 | */ 535 | openPath(path: string): Promise; 536 | /** 537 | * Resolves the shortcut link at `shortcutPath`. 538 | * 539 | * An exception will be thrown when any error happens. 540 | * 541 | * @platform win32 542 | */ 543 | readShortcutLink(shortcutPath: string): ShortcutDetails; 544 | /** 545 | * Show the given file in a file manager. If possible, select the file. 546 | */ 547 | showItemInFolder(fullPath: string): void; 548 | /** 549 | * Resolves when the operation has been completed. Rejects if there was an error 550 | * while deleting the requested item. 551 | * 552 | * This moves a path to the OS-specific trash location (Trash on macOS, Recycle Bin 553 | * on Windows, and a desktop-environment-specific location on Linux). 554 | */ 555 | trashItem(path: string): Promise; 556 | /** 557 | * Whether the shortcut was created successfully. 558 | * 559 | * Creates or updates a shortcut link at `shortcutPath`. 560 | * 561 | * @platform win32 562 | */ 563 | writeShortcutLink(shortcutPath: string, operation: 'create' | 'update' | 'replace', options: ShortcutDetails): boolean; 564 | /** 565 | * Whether the shortcut was created successfully. 566 | * 567 | * Creates or updates a shortcut link at `shortcutPath`. 568 | * 569 | * @platform win32 570 | */ 571 | writeShortcutLink(shortcutPath: string, options: ShortcutDetails): boolean; 572 | } 573 | interface WebFrame extends NodeJS.EventEmitter { 574 | 575 | // Docs: https://electronjs.org/docs/api/web-frame 576 | 577 | /** 578 | * Attempts to free memory that is no longer being used (like images from a 579 | * previous navigation). 580 | * 581 | * Note that blindly calling this method probably makes Electron slower since it 582 | * will have to refill these emptied caches, you should only call it if an event in 583 | * your app has occurred that makes you think your page is actually using less 584 | * memory (i.e. you have navigated from a super heavy page to a mostly empty one, 585 | * and intend to stay there). 586 | */ 587 | clearCache(): void; 588 | /** 589 | * A promise that resolves with the result of the executed code or is rejected if 590 | * execution throws or results in a rejected promise. 591 | * 592 | * Evaluates `code` in page. 593 | * 594 | * In the browser window some HTML APIs like `requestFullScreen` can only be 595 | * invoked by a gesture from the user. Setting `userGesture` to `true` will remove 596 | * this limitation. 597 | */ 598 | executeJavaScript(code: string, userGesture?: boolean, callback?: (result: any, error: Error) => void): Promise; 599 | /** 600 | * A promise that resolves with the result of the executed code or is rejected if 601 | * execution could not start. 602 | * 603 | * Works like `executeJavaScript` but evaluates `scripts` in an isolated context. 604 | * 605 | * Note that when the execution of script fails, the returned promise will not 606 | * reject and the `result` would be `undefined`. This is because Chromium does not 607 | * dispatch errors of isolated worlds to foreign worlds. 608 | */ 609 | executeJavaScriptInIsolatedWorld(worldId: number, scripts: WebSource[], userGesture?: boolean, callback?: (result: any, error: Error) => void): Promise; 610 | /** 611 | * A child of `webFrame` with the supplied `name`, `null` would be returned if 612 | * there's no such frame or if the frame is not in the current renderer process. 613 | */ 614 | findFrameByName(name: string): WebFrame; 615 | /** 616 | * that has the supplied `routingId`, `null` if not found. 617 | */ 618 | findFrameByRoutingId(routingId: number): WebFrame; 619 | /** 620 | * The frame element in `webFrame's` document selected by `selector`, `null` would 621 | * be returned if `selector` does not select a frame or if the frame is not in the 622 | * current renderer process. 623 | */ 624 | getFrameForSelector(selector: string): WebFrame; 625 | /** 626 | * * `images` MemoryUsageDetails 627 | * * `scripts` MemoryUsageDetails 628 | * * `cssStyleSheets` MemoryUsageDetails 629 | * * `xslStyleSheets` MemoryUsageDetails 630 | * * `fonts` MemoryUsageDetails 631 | * * `other` MemoryUsageDetails 632 | * 633 | * Returns an object describing usage information of Blink's internal memory 634 | * caches. 635 | * 636 | * This will generate: 637 | */ 638 | getResourceUsage(): ResourceUsage; 639 | /** 640 | * A list of suggested words for a given word. If the word is spelled correctly, 641 | * the result will be empty. 642 | */ 643 | getWordSuggestions(word: string): string[]; 644 | /** 645 | * The current zoom factor. 646 | */ 647 | getZoomFactor(): number; 648 | /** 649 | * The current zoom level. 650 | */ 651 | getZoomLevel(): number; 652 | /** 653 | * A key for the inserted CSS that can later be used to remove the CSS via 654 | * `webFrame.removeInsertedCSS(key)`. 655 | * 656 | * Injects CSS into the current web page and returns a unique key for the inserted 657 | * stylesheet. 658 | */ 659 | insertCSS(css: string, options?: InsertCSSOptions): string; 660 | /** 661 | * Inserts `text` to the focused element. 662 | */ 663 | insertText(text: string): void; 664 | /** 665 | * True if the word is misspelled according to the built in spellchecker, false 666 | * otherwise. If no dictionary is loaded, always return false. 667 | */ 668 | isWordMisspelled(word: string): boolean; 669 | /** 670 | * Removes the inserted CSS from the current web page. The stylesheet is identified 671 | * by its key, which is returned from `webFrame.insertCSS(css)`. 672 | */ 673 | removeInsertedCSS(key: string): void; 674 | /** 675 | * Set the security origin, content security policy and name of the isolated world. 676 | * Note: If the `csp` is specified, then the `securityOrigin` also has to be 677 | * specified. 678 | */ 679 | setIsolatedWorldInfo(worldId: number, info: Info): void; 680 | setSpellCheckProvider(language: string, provider: Provider): void; 681 | /** 682 | * Sets the maximum and minimum pinch-to-zoom level. 683 | * 684 | * > **NOTE**: Visual zoom is disabled by default in Electron. To re-enable it, 685 | * call: 686 | * 687 | * > **NOTE**: Visual zoom only applies to pinch-to-zoom behavior. Cmd+/-/0 zoom 688 | * shortcuts are controlled by the 'zoomIn', 'zoomOut', and 'resetZoom' MenuItem 689 | * roles in the application Menu. To disable shortcuts, manually define the Menu 690 | * and omit zoom roles from the definition. 691 | */ 692 | setVisualZoomLevelLimits(minimumLevel: number, maximumLevel: number): void; 693 | /** 694 | * Changes the zoom factor to the specified factor. Zoom factor is zoom percent 695 | * divided by 100, so 300% = 3.0. 696 | * 697 | * The factor must be greater than 0.0. 698 | */ 699 | setZoomFactor(factor: number): void; 700 | /** 701 | * Changes the zoom level to the specified level. The original size is 0 and each 702 | * increment above or below represents zooming 20% larger or smaller to default 703 | * limits of 300% and 50% of original size, respectively. 704 | * 705 | * > **NOTE**: The zoom policy at the Chromium level is same-origin, meaning that 706 | * the zoom level for a specific domain propagates across all instances of windows 707 | * with the same domain. Differentiating the window URLs will make zoom work 708 | * per-window. 709 | */ 710 | setZoomLevel(level: number): void; 711 | /** 712 | * A `WebFrame | null` representing the first child frame of `webFrame`, the 713 | * property would be `null` if `webFrame` has no children or if first child is not 714 | * in the current renderer process. 715 | * 716 | */ 717 | readonly firstChild: (WebFrame) | (null); 718 | /** 719 | * A `WebFrame | null` representing next sibling frame, the property would be 720 | * `null` if `webFrame` is the last frame in its parent or if the next sibling is 721 | * not in the current renderer process. 722 | * 723 | */ 724 | readonly nextSibling: (WebFrame) | (null); 725 | /** 726 | * A `WebFrame | null` representing the frame which opened `webFrame`, the property 727 | * would be `null` if there's no opener or opener is not in the current renderer 728 | * process. 729 | * 730 | */ 731 | readonly opener: (WebFrame) | (null); 732 | /** 733 | * A `WebFrame | null` representing parent frame of `webFrame`, the property would 734 | * be `null` if `webFrame` is top or parent is not in the current renderer process. 735 | * 736 | */ 737 | readonly parent: (WebFrame) | (null); 738 | /** 739 | * An `Integer` representing the unique frame id in the current renderer process. 740 | * Distinct WebFrame instances that refer to the same underlying frame will have 741 | * the same `routingId`. 742 | * 743 | */ 744 | readonly routingId: number; 745 | /** 746 | * A `WebFrame | null` representing top frame in frame hierarchy to which 747 | * `webFrame` belongs, the property would be `null` if top frame is not in the 748 | * current renderer process. 749 | * 750 | */ 751 | readonly top: (WebFrame) | (null); 752 | } 753 | 754 | declare var electron: { 755 | clipboard: Clipboard 756 | contextBridge: ContextBridge 757 | crashReporter: CrashReporter 758 | ipcRenderer: IpcRenderer 759 | nativeImage: NativeImage 760 | shell: Shell 761 | webFrame: WebFrame 762 | } 763 | -------------------------------------------------------------------------------- /src/common/monaco/types/utools.api.d.ts: -------------------------------------------------------------------------------- 1 | interface UBrowser { 2 | /** 3 | * 设置 User-Agent 4 | */ 5 | useragent(userAgent: string): this 6 | /** 7 | * 前往 8 | * @param url 链接地址,支持 http 或 file 协议 9 | * @param headers 请求头参数 10 | * @param timeout 加载超时,默认 60000 ms(60秒) 11 | */ 12 | goto(url: string, headers?: { Referer: string; userAgent: string }, timeout?: number): this 13 | /** 14 | * 页面大小 15 | */ 16 | viewport(width: number, height: number): this 17 | /** 18 | * 隐藏 ubrowser 窗口 19 | */ 20 | hide(): this 21 | /** 22 | * 显示 ubrowser 窗口 23 | */ 24 | show(): this 25 | /** 26 | * 注入样式 27 | */ 28 | css(css: string): this 29 | /** 30 | * 键盘按键 31 | */ 32 | press( 33 | key: string, 34 | ...modifier: ('control' | 'ctrl' | 'shift' | 'meta' | 'alt' | 'command' | 'cmd')[] 35 | ): this 36 | /** 37 | * 粘贴 38 | * @param text 如果是图片的base64编码字符串,粘贴图片,为空只执行粘贴动作 39 | */ 40 | paste(text?: string): this 41 | /** 42 | * 页面截图 43 | * @param arg 1.字符串 - 要截取的DOM元素, 2.对象 - 截图位置和大小, 3.空 - 为截取整个窗口 44 | * @param savePath 截图保存路径,可以是文件夹 或 .png文件完全路径, 默认保存临时目录 45 | */ 46 | screenshot( 47 | arg: string | { x: number; y: number; width: number; height: number }, 48 | savePath?: string 49 | ): this 50 | /** 51 | * 保存为PDF 52 | * @param options 选项 53 | * @param savePath PDF保存路径,可以是文件夹 或 .pdf文件完全路径, 默认保存临时目录 54 | */ 55 | pdf( 56 | options?: { 57 | marginsType: 0 | 1 | 2 58 | pageSize: 59 | | ('A3' | 'A4' | 'A5' | 'Legal' | 'Letter' | 'Tabloid') 60 | | { width: number; height: number } 61 | }, 62 | savePath?: string 63 | ): this 64 | /** 65 | * 模拟设备 66 | */ 67 | device( 68 | arg: 69 | | ( 70 | | 'iPhone 11' 71 | | 'iPhone X' 72 | | 'iPad' 73 | | 'iPhone 6/7/8 Plus' 74 | | 'iPhone 6/7/8' 75 | | 'iPhone 5/SE' 76 | | 'HUAWEI Mate10' 77 | | 'HUAWEI Mate20' 78 | | 'HUAWEI Mate30' 79 | | 'HUAWEI Mate30 Pro' 80 | ) 81 | | { size: { width: number; height: number }; useragent: string } 82 | ): this 83 | /** 84 | * 获取 cookie 85 | * @param name 为空获取全部cookie 86 | */ 87 | cookies(name?: string): this 88 | /** 89 | * 设置Cookie 90 | */ 91 | setCookies(name: string, value: string): this 92 | /** 93 | * 设置Cookie 94 | */ 95 | setCookies(cookies: { name: string; value: string }[]): this 96 | /** 97 | * 删除 cookie 98 | */ 99 | removeCookies(name: string): this 100 | /** 101 | * 清空cookie 102 | * @param url 在执行"goto"前执行 url参数必需 103 | */ 104 | clearCookies(url?: string): this 105 | /** 106 | * 打开开发者工具 107 | */ 108 | devTools(mode?: 'right' | 'bottom' | 'undocked' | 'detach'): this 109 | /** 110 | * 执行JS计算 并获得结果 111 | * @param func 在目标网页中执行 112 | * @param params 传到 func 中的参数 113 | */ 114 | evaluate(func: (...params: any[]) => any, ...params: any[]): this 115 | /** 116 | * 等待时间 117 | * @param ms 毫秒 118 | */ 119 | wait(ms: number): this 120 | /** 121 | * 等待元素出现 122 | * @param selector DOM元素 123 | * @param timeout 超时 默认60000 ms(60秒) 124 | */ 125 | wait(selector: string, timeout?: number): this 126 | /** 127 | * 等待 JS函数 执行返回 true 128 | * @param func 执行的JS函数 129 | * @param timeout 超时 默认60000 ms(60秒) 130 | * @param params 传到 func 中的参数 131 | */ 132 | wait(func: (...params: any[]) => boolean, timeout?: number, ...params: any[]): this 133 | /** 134 | * 当元素存在时执行直到碰到 end 135 | * @param selector DOM元素 136 | */ 137 | when(selector: string): this 138 | /** 139 | * 当 JS函数执行返回 true 时执行直到碰到 end 140 | * @param func 执行的JS函数 141 | * @param params 传到 func 中的参数 142 | */ 143 | when(func: (...params: any[]) => boolean, ...params: any[]): this 144 | /** 145 | * 配合 when 使用 146 | */ 147 | end(): this 148 | /** 149 | * 单击元素 150 | */ 151 | click(selector: string): this 152 | /** 153 | * 元素触发按下鼠标左键 154 | */ 155 | mousedown(selector: string): this 156 | /** 157 | * 元素触发释放鼠标左键 158 | */ 159 | mouseup(selector: string): this 160 | /** 161 | * 赋值 file input 162 | * @param selector 元素 163 | * @param payload 1. string - 文件路径 或 图片的base64编码,2. string[] - 文件路径集合,3. Uint8Array[] 164 | */ 165 | file(selector: string, payload: string | string[] | Uint8Array): this 166 | /** 167 | * input textarea select 等元素赋值并触发 input 或 change事件 168 | */ 169 | value(selector: string, value: string): this 170 | /** 171 | * checkbox radio 元素选中或取消选中 172 | */ 173 | check(selector: string, checked: boolean): this 174 | /** 175 | * 元素获得焦点 176 | */ 177 | focus(selector: string): this 178 | /** 179 | * 滚动到元素位置 180 | */ 181 | scroll(selector: string): this 182 | /** 183 | * Y轴滚动 184 | */ 185 | scroll(y: number): this 186 | /** 187 | * X轴和Y轴滚动 188 | */ 189 | scroll(x: number, y: number): this 190 | /** 191 | * 运行在闲置的 ubrowser 上 192 | * @param ubrowserId utools.getIdleUBrowsers() 中获得 193 | */ 194 | run(ubrowserId: number): Promise 195 | /** 196 | * 启动一个 ubrowser 运行 197 | * 当运行结束后,窗口如果为隐藏状态将自动销毁窗口 198 | * @param options 199 | */ 200 | run(options: { 201 | show?: boolean 202 | width?: number 203 | height?: number 204 | x?: number 205 | y?: number 206 | center?: boolean 207 | minWidth?: number 208 | minHeight?: number 209 | maxWidth?: number 210 | maxHeight?: number 211 | resizable?: boolean 212 | movable?: boolean 213 | minimizable?: boolean 214 | maximizable?: boolean 215 | alwaysOnTop?: boolean 216 | fullscreen?: boolean 217 | fullscreenable?: boolean 218 | enableLargerThanScreen?: boolean 219 | opacity?: number 220 | }): Promise 221 | } 222 | 223 | interface Display { 224 | accelerometerSupport: 'available' | 'unavailable' | 'unknown' 225 | bounds: { x: number; y: number; width: number; height: number } 226 | colorDepth: number 227 | colorSpace: string 228 | depthPerComponent: number 229 | id: number 230 | internal: boolean 231 | monochrome: boolean 232 | rotation: number 233 | scaleFactor: number 234 | size: { width: number; height: number } 235 | touchSupport: 'available' | 'unavailable' | 'unknown' 236 | workArea: { x: number; y: number; width: number; height: number } 237 | workAreaSize: { width: number; height: number } 238 | } 239 | 240 | interface DbDoc { 241 | _id: string 242 | _rev?: string 243 | [key: string]: any 244 | } 245 | 246 | interface DbReturn { 247 | id: string 248 | rev?: string 249 | ok?: boolean 250 | error?: boolean 251 | name?: string 252 | message?: string 253 | } 254 | 255 | interface UToolsApi { 256 | /** 257 | * 插件应用进入时触发 258 | */ 259 | // onPluginEnter(callback: (action: { code: string; type: string; payload: any }) => void): void 260 | /** 261 | * 插件应用隐藏后台或完全退出时触发 262 | */ 263 | // onPluginOut(callback: (processExit: boolean) => void): void 264 | /** 265 | * 插件应用分离时触发 266 | */ 267 | // onPluginDetach(callback: () => void): void 268 | /** 269 | * 插件应用从云端拉取到数据时触发 270 | */ 271 | // onDbPull(callback: (docs: { _id: string; _rev: string }[]) => void): void 272 | /** 273 | * 隐藏主窗口 274 | * @param isRestorePreWindow 是否焦点回归到前面的活动窗口,默认 true 275 | */ 276 | hideMainWindow(isRestorePreWindow?: boolean): boolean 277 | /** 278 | * 显示主窗口 279 | */ 280 | showMainWindow(): boolean 281 | /** 282 | * 设置插件应用自身高度 283 | */ 284 | setExpendHeight(height: number): boolean 285 | /** 286 | * 设置子输入框 287 | * @param onChange 修改时触发 288 | * @param placeholder 占位符, 默认为空 289 | * @param isFocus 是否获得焦点,默认为 true 290 | */ 291 | setSubInput(onChange: (text: string) => void, placeholder?: string, isFocus?: boolean): boolean 292 | /** 293 | * 移除子输入框 294 | */ 295 | removeSubInput(): boolean 296 | /** 297 | * 赋值子输入框 298 | */ 299 | setSubInputValue(value: string): boolean 300 | /** 301 | * 子输入框获得焦点 302 | */ 303 | subInputFocus(): boolean 304 | /** 305 | * 子输入框获得焦点并选中 306 | */ 307 | subInputSelect(): boolean 308 | /** 309 | * 子输入框失去焦点,插件应用获得焦点 310 | */ 311 | subInputBlur(): boolean 312 | /** 313 | * 创建独立窗口 314 | * @param url 相对路径 html 文件 315 | * @param options 参考 https://www.electronjs.org/docs/api/browser-window#new-browserwindowoptions 316 | * @param callback url 加载完成时的回调 317 | */ 318 | // createBrowserWindow( 319 | // url: string, 320 | // options: { width?: number; height?: number }, 321 | // callback?: () => void 322 | // ): { id: number; [key: string]: any; webContents: { id: number; [key: string]: any } } 323 | /** 324 | * 隐藏插件应用到后台 325 | */ 326 | outPlugin(): boolean 327 | /** 328 | * 是否深色模式 329 | */ 330 | isDarkColors(): boolean 331 | /** 332 | * 获取用户 333 | */ 334 | getUser(): { avatar: string; nickname: string; type: string } | null 335 | /** 336 | * 获取用户服务端临时令牌 337 | */ 338 | // fetchUserServerTemporaryToken(): Promise<{ token: string; expiredAt: number }> 339 | /** 340 | * 打开支付 341 | * @param callback 支付成功触发 342 | */ 343 | // openPayment( 344 | // options: { 345 | // /** 346 | // * 商品ID,在 “uTools 开发者工具” 插件应用中创建 347 | // */ 348 | // goodsId: string 349 | // /** 350 | // * 第三方服务生成的订单号(可选) 351 | // */ 352 | // outOrderId?: string 353 | // /** 354 | // * 第三方服务附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用(可选) 355 | // */ 356 | // attach?: string 357 | // }, 358 | // callback?: () => void 359 | // ): void 360 | /** 361 | * 获取用户支付记录 362 | */ 363 | // fetchUserPayments(): Promise< 364 | // { 365 | // order_id: string 366 | // total_fee: number 367 | // body: string 368 | // attach: string 369 | // goods_id: string 370 | // out_order_id: string 371 | // paid_at: string 372 | // }[] 373 | // > 374 | /** 375 | * 设置插件应用动态功能 376 | */ 377 | // setFeature(feature: { 378 | // code: string 379 | // explain: string 380 | // platform: ('darwin' | 'win32' | 'linux') | Array<'darwin' | 'win32' | 'linux'> 381 | // icon?: string 382 | // cmds: ( 383 | // | string 384 | // | { 385 | // type: 'img' | 'files' | 'regex' | 'over' | 'window' 386 | // label: string 387 | // } 388 | // )[] 389 | // }): boolean 390 | /** 391 | * 移除插件应用动态功能 392 | */ 393 | // removeFeature(code: string): boolean 394 | /** 395 | * 获取插件应用动态功能,参数为空获取所有动态功能 396 | */ 397 | // getFeatures(codes?: string[]): { 398 | // code: string 399 | // explain: string 400 | // platform: ('darwin' | 'win32' | 'linux') | Array<'darwin' | 'win32' | 'linux'> 401 | // icon?: string 402 | // cmds: 403 | // | string 404 | // | { 405 | // type: 'img' | 'files' | 'regex' | 'over' | 'window' 406 | // label: string 407 | // }[] 408 | // }[] 409 | /** 410 | * 插件应用间跳转 411 | */ 412 | redirect(label: string, payload: string | { type: 'text' | 'img' | 'files'; data: any }): void 413 | /** 414 | * 获取闲置的 ubrowser 415 | */ 416 | getIdleUBrowsers(): { id: number; title: string; url: string }[] 417 | /** 418 | * 设置 ubrowser 代理 https://www.electronjs.org/docs/api/session#sessetproxyconfig 419 | */ 420 | setUBrowserProxy(config: { 421 | pacScript?: string 422 | proxyRules?: string 423 | proxyBypassRules?: string 424 | }): boolean 425 | /** 426 | * 清空 ubrowser 缓存 427 | */ 428 | clearUBrowserCache(): boolean 429 | /** 430 | * 显示系统通知 431 | */ 432 | showNotification(body: string): void 433 | /** 434 | * 弹出文件选择框 435 | */ 436 | showOpenDialog(options: { 437 | title?: string 438 | defaultPath?: string 439 | buttonLabel?: string 440 | filters?: { name: string; extensions: string[] }[] 441 | properties?: Array< 442 | | 'openFile' 443 | | 'openDirectory' 444 | | 'multiSelections' 445 | | 'showHiddenFiles' 446 | | 'createDirectory' 447 | | 'promptToCreate' 448 | | 'noResolveAliases' 449 | | 'treatPackageAsDirectory' 450 | | 'dontAddToRecent' 451 | > 452 | message?: string 453 | securityScopedBookmarks?: boolean 454 | }): string[] | undefined 455 | /** 456 | * 弹出文件保存框 457 | */ 458 | showSaveDialog(options: { 459 | title?: string 460 | defaultPath?: string 461 | buttonLabel?: string 462 | filters?: { name: string; extensions: string[] }[] 463 | message?: string 464 | nameFieldLabel?: string 465 | showsTagField?: string 466 | properties?: Array< 467 | | 'showHiddenFiles' 468 | | 'createDirectory' 469 | | 'treatPackageAsDirectory' 470 | | 'showOverwriteConfirmation' 471 | | 'dontAddToRecent' 472 | > 473 | securityScopedBookmarks?: boolean 474 | }): string | undefined 475 | /** 476 | * 插件应用页面中查找 477 | */ 478 | findInPage( 479 | text: string, 480 | options?: { 481 | forward?: boolean 482 | findNext?: boolean 483 | matchCase?: boolean 484 | wordStart?: boolean 485 | medialCapitalAsWordStart?: boolean 486 | } 487 | ): void 488 | /** 489 | * 停止插件应用页面中查找 490 | */ 491 | stopFindInPage(action: 'clearSelection' | 'keepSelection' | 'activateSelection'): void 492 | /** 493 | * 拖拽文件 494 | */ 495 | startDrag(file: string | string[]): void 496 | /** 497 | * 屏幕取色 498 | */ 499 | screenColorPick(callback: (color: { hex: string; rgb: string }) => void): void 500 | /** 501 | * 屏幕截图 502 | */ 503 | screenCapture(callback: (imgBase64: string) => void): void 504 | /** 505 | * 获取本地 ID 506 | */ 507 | getNativeId(): string 508 | /** 509 | * 获取软件版本 510 | */ 511 | getAppVersion(): string 512 | /** 513 | * 获取路径 514 | */ 515 | getPath( 516 | name: 517 | | 'home' 518 | | 'appData' 519 | | 'userData' 520 | | 'cache' 521 | | 'temp' 522 | | 'exe' 523 | | 'module' 524 | | 'desktop' 525 | | 'documents' 526 | | 'downloads' 527 | | 'music' 528 | | 'pictures' 529 | | 'videos' 530 | | 'logs' 531 | | 'pepperFlashSystemPlugin' 532 | ): string 533 | /** 534 | * 获取文件图标 535 | */ 536 | getFileIcon(filePath: string): string 537 | /** 538 | * 复制文件到剪贴板 539 | */ 540 | copyFile(file: string | string[]): boolean 541 | /** 542 | * 复制图片到剪贴板 543 | * @param img base64、buffer、图片路径 544 | */ 545 | copyImage(img: string | Uint8Array): boolean 546 | /** 547 | * 复制文本到剪贴板 548 | */ 549 | copyText(text: string): boolean 550 | /** 551 | * 获取复制的文件或文件夹 552 | */ 553 | getCopyedFiles(): { isFile: boolean; isDirectory: boolean; name: string; path: string }[] 554 | /** 555 | * 读取当前文件管理器路径(linux 不支持) 556 | */ 557 | readCurrentFolderPath(): Promise 558 | /** 559 | * 读取当前浏览器窗口的URL(linux 不支持) 560 | * MacOs 支持浏览器 Safari、Chrome、Opera、Vivaldi、Brave 561 | * Windows 支持浏览器 Chrome、Firefox、Edge、IE、Opera、Brave 562 | * Linux 不支持 563 | */ 564 | readCurrentBrowserUrl(): Promise 565 | /** 566 | * 默认方式打开给定的文件 567 | */ 568 | shellOpenPath(fullPath: string): void 569 | /** 570 | * 在文件管理器中显示给定的文件 571 | */ 572 | shellShowItemInFolder(fullPath: string): void 573 | /** 574 | * 系统默认的协议打开URL 575 | */ 576 | shellOpenExternal(url: string): void 577 | /** 578 | * 播放哔哔声 579 | */ 580 | shellBeep(): void 581 | /* 582 | * 隐藏主窗口并键入字符串 583 | */ 584 | hideMainWindowTypeString(str: string): void 585 | /** 586 | * 模拟键盘按键 587 | */ 588 | simulateKeyboardTap( 589 | key: string, 590 | ...modifier: ('control' | 'ctrl' | 'shift' | 'option' | 'alt' | 'command' | 'super')[] 591 | ): void 592 | /** 593 | * 模拟鼠标单击 594 | */ 595 | simulateMouseClick(x?: number, y?: number): void 596 | /** 597 | * 模拟鼠标右击 598 | */ 599 | simulateMouseRightClick(x?: number, y?: number): void 600 | /** 601 | * 模拟鼠标双击 602 | */ 603 | simulateMouseDoubleClick(x?: number, y?: number): void 604 | /** 605 | * 模拟鼠标移动 606 | */ 607 | simulateMouseMove(x: number, y: number): void 608 | /** 609 | * 获取鼠标绝对位置 610 | */ 611 | getCursorScreenPoint(): { x: number; y: number } 612 | /** 613 | * 获取主显示器 614 | */ 615 | getPrimaryDisplay(): Display 616 | /** 617 | * 获取所有显示器 618 | */ 619 | getAllDisplays(): Display[] 620 | /** 621 | * 获取位置所在的显示器 622 | */ 623 | getDisplayNearestPoint(point: { x: number; y: number }): Display 624 | /** 625 | * 获取矩形所在的显示器 626 | */ 627 | getDisplayMatching(rect: { x: number; y: number; width: number; height: number }): Display 628 | /** 629 | * 录屏源 630 | */ 631 | desktopCaptureSources(options: { 632 | types: string[] 633 | thumbnailSize?: { width: number; height: number } 634 | fetchWindowIcons?: boolean 635 | }): Promise<{ appIcon: {}; display_id: string; id: string; name: string; thumbnail: {} }> 636 | /** 637 | * 是否开发中 638 | */ 639 | isDev(): boolean 640 | /** 641 | * 是否 MacOs 操作系统 642 | */ 643 | isMacOS(): boolean 644 | /** 645 | * 是否 Windows 操作系统 646 | */ 647 | isWindows(): boolean 648 | /** 649 | * 是否 Linux 操作系统 650 | */ 651 | isLinux(): boolean 652 | 653 | // db: { 654 | // /** 655 | // * 创建/更新文档 656 | // */ 657 | // put(doc: DbDoc): DbReturn 658 | // /** 659 | // * 获取文档 660 | // */ 661 | // get(id: string): DbDoc | null 662 | // /** 663 | // * 删除文档 664 | // */ 665 | // remove(doc: string | DbDoc): DbReturn 666 | // /** 667 | // * 批量操作文档(新增、修改、删除) 668 | // */ 669 | // bulkDocs(docs: DbDoc[]): DbReturn[] 670 | // /** 671 | // * 获取所有文档 可根据文档id前缀查找 672 | // */ 673 | // allDocs(key?: string): DbDoc[] 674 | // /** 675 | // * 存储附件到新文档 676 | // * @param docId 文档ID 677 | // * @param attachment 附件 buffer 678 | // * @param type 附件类型,示例:image/png, text/plain 679 | // */ 680 | // postAttachment(docId: string, attachment: Uint8Array, type: string): DbReturn 681 | // /** 682 | // * 获取附件 683 | // * @param docId 文档ID 684 | // */ 685 | // getAttachment(docId: string): Uint8Array | null 686 | // /** 687 | // * 获取附件类型 688 | // * @param docId 文档ID 689 | // */ 690 | // getAttachmentType(docId: string): string | null 691 | // /** 692 | // * 云端复制数据状态 (null: 未开启数据同步、0: 已完成复制、1:复制中) 693 | // */ 694 | // replicateStateFromCloud(): null | 0 | 1 695 | // /** 696 | // * 异步 697 | // */ 698 | // promises: { 699 | // /** 700 | // * 创建/更新文档 701 | // */ 702 | // put(doc: DbDoc): Promise 703 | // /** 704 | // * 获取文档 705 | // */ 706 | // get(id: string): Promise 707 | // /** 708 | // * 删除文档 709 | // */ 710 | // remove(doc: string | DbDoc): Promise 711 | // /** 712 | // * 批量操作文档(新增、修改、删除) 713 | // */ 714 | // bulkDocs(docs: DbDoc[]): Promise 715 | // /** 716 | // * 获取所有文档 可根据文档id前缀查找 717 | // */ 718 | // allDocs(key?: string): Promise 719 | // /** 720 | // * 存储附件到新文档 721 | // * @param docId 文档ID 722 | // * @param attachment 附件 buffer 723 | // * @param type 附件类型,示例:image/png, text/plain 724 | // */ 725 | // postAttachment(docId: string, attachment: Uint8Array, type: string): Promise 726 | // /** 727 | // * 获取附件 728 | // * @param docId 文档ID 729 | // */ 730 | // getAttachment(docId: string): Promise 731 | // /** 732 | // * 获取附件类型 733 | // * @param docId 文档ID 734 | // */ 735 | // getAttachmentType(docId: string): Promise 736 | // /** 737 | // * 云端复制数据状态 (null: 未开启数据同步、0: 已完成复制、1:复制中) 738 | // */ 739 | // replicateStateFromCloud(): Promise 740 | // } 741 | // } 742 | 743 | // dbStorage: { 744 | // /** 745 | // * 键值对存储,如果键名存在,则更新其对应的值 746 | // * @param key 键名(同时为文档ID) 747 | // * @param value 键值 748 | // */ 749 | // setItem(key: string, value: any): void 750 | // /** 751 | // * 获取键名对应的值 752 | // */ 753 | // getItem(key: string): any 754 | // /** 755 | // * 删除键值对(删除文档) 756 | // */ 757 | // removeItem(key: string): void 758 | // } 759 | 760 | ubrowser: UBrowser 761 | } 762 | 763 | declare var utools: UToolsApi 764 | -------------------------------------------------------------------------------- /src/common/registerCallback.ts: -------------------------------------------------------------------------------- 1 | import { Message } from '@arco-design/web-vue' 2 | import { isElectron } from '@/utils' 3 | 4 | export function registerCallback() { 5 | if (!isElectron) return 6 | 7 | if (utools.db.replicateStateFromCloud()) { 8 | Message.warning({ 9 | content: '数据可能不完整,还在从云端复制中' 10 | }) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/common/registerIcon.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue' 2 | import { 3 | IconCopy, 4 | IconFile, 5 | IconInfoCircle, 6 | IconPlus, 7 | IconThunderbolt, 8 | IconSettings, 9 | IconCloseCircle, 10 | IconDown, 11 | IconEdit, 12 | IconCommand, 13 | IconEye, 14 | IconDelete, 15 | IconShareAlt, 16 | IconLock, 17 | IconUnlock 18 | } from '@arco-design/web-vue/es/icon' 19 | 20 | const icons = [ 21 | IconCopy, 22 | IconFile, 23 | IconInfoCircle, 24 | IconPlus, 25 | IconThunderbolt, 26 | IconSettings, 27 | IconCloseCircle, 28 | IconDown, 29 | IconEdit, 30 | IconCommand, 31 | IconEye, 32 | IconDelete, 33 | IconShareAlt, 34 | IconLock, 35 | IconUnlock 36 | ] 37 | 38 | export function registerIcon(app: App) { 39 | for (const icon of icons) { 40 | app.component(icon.name, icon) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/common/symbol.ts: -------------------------------------------------------------------------------- 1 | import { InjectionKey } from 'vue' 2 | 3 | /** 4 | * Whether the current theme is dark 5 | */ 6 | export const IS_DARK = Symbol('IS_DARK') as InjectionKey> 7 | 8 | /** 9 | * Event: focus editor 10 | */ 11 | export const EVENT_FOCUS_EDITOR = Symbol('EVENT_FOCUS_EDITOR') 12 | -------------------------------------------------------------------------------- /src/components/CodeList.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 46 | 47 | 72 | -------------------------------------------------------------------------------- /src/components/Console.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 136 | 137 | 185 | -------------------------------------------------------------------------------- /src/components/Editor.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 183 | 184 | 190 | -------------------------------------------------------------------------------- /src/components/SettingContent.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | -------------------------------------------------------------------------------- /src/components/ShortCut.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 78 | 79 | 88 | -------------------------------------------------------------------------------- /src/hooks/useEventBus.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @desc 统一管理 Eventbus 以约束类型 自动注册和移除事件 3 | */ 4 | 5 | // @ts-ignore 6 | import emitter from 'tiny-emitter/instance' 7 | import { onUnmounted } from 'vue' 8 | 9 | export const $on = (event: symbol, ...args: any[]) => emitter.on(event, ...args) 10 | export const $emit = (event: symbol, ...args: any[]) => emitter.emit(event, ...args) 11 | export const $off = (event: symbol, ...args: any[]) => emitter.off(event, ...args) 12 | export const $once = (event: symbol, ...args: any[]) => emitter.once(event, ...args) 13 | 14 | export function useEventBus(type: symbol, listner: Function) { 15 | $on(type, listner) 16 | 17 | // 自动移除事件 18 | if (getCurrentInstance()) { 19 | onUnmounted(() => $off(type, listner)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/hooks/useEventListener.ts: -------------------------------------------------------------------------------- 1 | export function useEventListener( 2 | target: EventTarget, 3 | type: string, 4 | listener: (event: any) => void, 5 | options?: boolean | AddEventListenerOptions 6 | ) { 7 | onMounted(() => { 8 | target.addEventListener(type, listener, options) 9 | }) 10 | 11 | onUnmounted(() => { 12 | target.removeEventListener(type, listener, options) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /src/hooks/useMedia.ts: -------------------------------------------------------------------------------- 1 | export function useMedia(query: string) { 2 | const matchList = window.matchMedia(query) 3 | const isMatch = ref(matchList.matches) 4 | 5 | const handler = (e: MediaQueryListEvent) => { 6 | isMatch.value = e.matches 7 | } 8 | 9 | matchList.addEventListener('change', handler) 10 | 11 | onUnmounted(() => { 12 | matchList.removeEventListener('change', handler) 13 | }) 14 | 15 | return isMatch 16 | } 17 | -------------------------------------------------------------------------------- /src/hooks/usePageBack.ts: -------------------------------------------------------------------------------- 1 | import { useEventListener } from '@/hooks/useEventListener' 2 | import { Button } from '@arco-design/web-vue' 3 | import { IconLeft } from '@arco-design/web-vue/es/icon' 4 | 5 | export function usePageBack() { 6 | const router = useRouter() 7 | const back = () => router.back() 8 | 9 | // 监听键盘事件 ESC执行返回 10 | useEventListener(document, 'keydown', (e: KeyboardEvent) => { 11 | if (e.key === 'Escape') { 12 | back() 13 | e.stopPropagation() 14 | } 15 | }) 16 | 17 | // return a function cause we need to use it in setup 18 | // if directly return a component, it will be called in render 19 | // and it will cause an warning: Non-function value encountered for default slot. 20 | return { 21 | BackButton: () => 22 | h( 23 | Button, 24 | { 25 | onClick: back, 26 | shape: 'circle', 27 | style: { 28 | position: 'fixed !important', 29 | top: '10px', 30 | left: '10px' 31 | } 32 | }, 33 | () => h(IconLeft) 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/hooks/usePrefersTheme.ts: -------------------------------------------------------------------------------- 1 | import type { Theme } from '@/store' 2 | import { useMedia } from '@/hooks/useMedia' 3 | 4 | export function usePrefersTheme(callback?: (theme: Theme) => void) { 5 | // MediaQuery theme 6 | const isDark = useMedia('(prefers-color-scheme: dark)') 7 | const isLight = useMedia('(prefers-color-scheme: light)') 8 | 9 | onMounted(() => setThemeMode(isDark.value ? 'dark' : 'light')) 10 | 11 | watch(isDark, (val) => { 12 | setThemeMode(val ? 'dark' : 'light') 13 | callback && callback(val ? 'dark' : 'light') 14 | }) 15 | 16 | return [isDark, isLight] 17 | } 18 | 19 | export function setThemeMode(theme: Theme) { 20 | if (theme === 'dark') { 21 | document.body.classList.add('dark') 22 | document.body.setAttribute('arco-theme', 'dark') 23 | } else if (theme === 'light') { 24 | document.body.classList.remove('dark') 25 | document.body.removeAttribute('arco-theme') 26 | } else { 27 | const [isDark] = usePrefersTheme() 28 | setThemeMode(isDark.value ? 'dark' : 'light') 29 | } 30 | return null 31 | } 32 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import '@/style/index.less' 3 | import App from './App.vue' 4 | import router from './router' 5 | import { createPinia } from 'pinia' 6 | import { registerDirectives } from '@/common/directives' 7 | import { registerIcon } from '@/common/registerIcon' 8 | import { registerCallback } from '@/common/registerCallback' 9 | import { isElectron, SERVER_DEV } from './utils' 10 | 11 | try { 12 | createApp(App) 13 | .use(router) 14 | .use(createPinia()) 15 | .use(registerDirectives) 16 | .use(registerIcon) 17 | .mount('#app') 18 | 19 | registerCallback() 20 | } catch (error) { 21 | console.error(error) 22 | if (isElectron) utools.showNotification('初始化失败: ' + error) 23 | } 24 | -------------------------------------------------------------------------------- /src/preload/index.ts: -------------------------------------------------------------------------------- 1 | import type { Buffer as _Buffer } from 'buffer' 2 | import type _vm from 'vm' 3 | 4 | declare global { 5 | interface Window { 6 | preload: { 7 | electron: any 8 | Buffer: typeof _Buffer 9 | require: NodeRequire 10 | vm: typeof _vm 11 | } 12 | } 13 | } 14 | 15 | const empty = () => null 16 | 17 | export const electron = window?.preload?.electron || {} 18 | export const Buffer = window?.preload?.Buffer || {} 19 | export const require = window?.preload?.require || empty 20 | export const vm = window?.preload?.vm || {} 21 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router' 2 | 3 | const router = createRouter({ 4 | history: createWebHashHistory(), 5 | routes: [ 6 | { 7 | path: '/', 8 | redirect: '/runner' 9 | }, 10 | { 11 | path: '/runner', 12 | component: () => import('@/views/Runner.vue') 13 | }, 14 | { 15 | path: '/setting', 16 | component: () => import('@/views/Setting.vue') 17 | }, 18 | { 19 | path: '/about', 20 | component: () => import('@/views/About.vue') 21 | } 22 | ] 23 | }) 24 | 25 | export default router 26 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useCodeStore' 2 | export * from './useSettingStore' 3 | export * from './useScriptStore' 4 | -------------------------------------------------------------------------------- /src/store/useCodeStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { uniqueId } from 'lodash-es' 3 | import { Message } from '@arco-design/web-vue' 4 | import { 5 | getItem, 6 | setItem, 7 | removeItem, 8 | runCodeInSandbox, 9 | runCodeInBrowser, 10 | isElectron 11 | } from '@/utils' 12 | import { IMessage } from '@/components/Console.vue' 13 | import { useHistoryStore } from './useHistoryStore' 14 | 15 | export interface ICodeStoreState { 16 | /** 17 | * code id 18 | */ 19 | id: number 20 | /** 21 | * code content 22 | */ 23 | code: string 24 | /** 25 | * console messages 26 | */ 27 | messages: IMessage[] 28 | /** 29 | * code running environment 30 | */ 31 | env: 'node' | 'browser' 32 | /** 33 | * code running mode 34 | */ 35 | mode: 'ontime' | 'manual' 36 | /** 37 | * code readonly 38 | */ 39 | readonly: boolean 40 | /** 41 | * code is running 42 | */ 43 | execState: boolean 44 | } 45 | 46 | export interface ICodeStore extends ReturnType {} 47 | 48 | export const useCodeStore = defineStore('CodeSrore', { 49 | state: () => 50 | ({ 51 | id: 0, 52 | code: '', 53 | messages: [] as IMessage[], 54 | env: isElectron ? getItem('env') || 'node' : 'browser', 55 | mode: getItem('mode') || 'ontime', 56 | readonly: getItem('readonly') || false, 57 | execState: false 58 | } as ICodeStoreState), 59 | getters: { 60 | codeWithId: (state) => `code/${state.id}`, 61 | currentEnv: (state) => (state.env === 'browser' ? '浏览器' : 'Node.js'), 62 | currentMode: (state) => (state.mode === 'ontime' ? '即时执行' : '手动触发'), 63 | currentReadonly: (state) => (state.readonly ? '只读' : '可编辑') 64 | }, 65 | actions: { 66 | init() { 67 | const lastCodeId = getItem('lastCodeId') || 0 68 | this.loadCode(lastCodeId) 69 | useHistoryStore().loadHistorys() 70 | }, 71 | 72 | newCode() { 73 | if (this.id !== 0) Message.success('成功创建新代码片段') 74 | this.id = new Date().getTime() 75 | this.code = getItem('lastCodeId') ? '' : 'console.log("Hello, World!")' 76 | setItem(this.codeWithId, this.code) 77 | setItem('lastCodeId', this.id) 78 | // 更新展示的历史记录数量 79 | useHistoryStore().loadHistorys() 80 | }, 81 | 82 | loadCode(id: number) { 83 | const code = getItem(`code/${id}`) 84 | if (code === null) { 85 | this.newCode() 86 | return 87 | } 88 | 89 | this.id = id 90 | this.code = code 91 | }, 92 | 93 | handleCodeChange(code: string) { 94 | this.code = code 95 | setItem(this.codeWithId, this.code) 96 | 97 | if (this.mode === 'ontime') this.execCode() 98 | }, 99 | 100 | pushMessage(message: IMessage) { 101 | message && this.messages.push(message) 102 | }, 103 | 104 | clearConsole() { 105 | this.messages = [] 106 | }, 107 | 108 | changeEnv() { 109 | if (!isElectron) return Message.warning('当前环境不支持切换运行环境') 110 | this.env = this.env === 'browser' ? 'node' : 'browser' 111 | this.env === 'browser' ? setItem('env', 'browser') : removeItem('env') 112 | }, 113 | 114 | changeMode() { 115 | this.mode = this.mode === 'manual' ? 'ontime' : 'manual' 116 | this.mode === 'manual' ? setItem('mode', 'manual') : removeItem('mode') 117 | }, 118 | 119 | changeReadonly() { 120 | this.readonly = !this.readonly 121 | this.readonly ? setItem('readonly', true) : removeItem('readonly') 122 | }, 123 | 124 | readHistory(timeStamp: number) { 125 | this.loadCode(timeStamp) 126 | setItem('lastCodeId', this.id) 127 | }, 128 | 129 | execCode() { 130 | if (!this.code) { 131 | return 132 | } 133 | 134 | const handleConsole = (stdout: any[], stderr: any[]) => { 135 | const id = uniqueId() 136 | const timeStamp = new Date().getTime() 137 | 138 | if (stdout && stdout?.length) 139 | this.pushMessage({ 140 | id, 141 | timeStamp, 142 | type: 'log', 143 | content: stdout 144 | }) 145 | if (stderr) this.pushMessage({ id, timeStamp, type: 'error', content: stderr }) 146 | } 147 | 148 | this.env === 'browser' 149 | ? runCodeInBrowser(this.code, handleConsole) 150 | : runCodeInSandbox(this.code, handleConsole) 151 | 152 | // side effect 153 | this.execState = true 154 | setTimeout(() => { 155 | this.execState = false 156 | }) 157 | } 158 | } 159 | }) 160 | -------------------------------------------------------------------------------- /src/store/useHistoryStore.ts: -------------------------------------------------------------------------------- 1 | import { Message } from '@arco-design/web-vue' 2 | import { allDocs, parseCommentBlock, removeItem } from '@/utils' 3 | import { useCodeStore } from './useCodeStore' 4 | 5 | export interface CodeHistory { 6 | id: string 7 | timeStamp: number 8 | code: string 9 | name?: string 10 | } 11 | 12 | export interface IHistoryStoreState { 13 | historys: CodeHistory[] 14 | } 15 | 16 | export const useHistoryStore = defineStore('history', { 17 | state: () => 18 | ({ 19 | historys: [] 20 | } as IHistoryStoreState), 21 | 22 | actions: { 23 | loadHistorys() { 24 | const res = allDocs('code/') 25 | 26 | if (!res || !res.length) return 27 | 28 | // newest at first 29 | res.sort((a: any, b: any) => parseInt(b._id.split('/')[1]) - parseInt(a._id.split('/')[1])) 30 | 31 | this.historys = res.map((item: any) => { 32 | // clear escape character in content and remove `"` at the beginning and end 33 | const plainCode = item.value 34 | 35 | const name = 36 | parseCommentBlock(plainCode)?.name || 37 | (plainCode.startsWith('//') ? plainCode.split('\n')[0].slice(2).trim() : '') 38 | 39 | return { 40 | id: item._id, 41 | timeStamp: parseInt(item._id.split('/')[1], 10), 42 | code: item.value, 43 | name 44 | } 45 | }) 46 | }, 47 | 48 | deleteHistory(timeStamp: number) { 49 | const idx = this.historys.findIndex((h) => h.timeStamp === timeStamp) 50 | this.historys.splice(idx, 1) 51 | removeItem(`code/${timeStamp}`) 52 | Message.success('删除成功') 53 | 54 | // 选中第一条历史记录 or 新建代码片段 55 | this.historys.length 56 | ? useCodeStore().readHistory(this.historys[0].timeStamp) 57 | : useCodeStore().newCode() 58 | } 59 | } 60 | }) 61 | -------------------------------------------------------------------------------- /src/store/useScriptStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import type { setFeature } from 'utools-api-types' 3 | import { Message } from '@arco-design/web-vue' 4 | import { setItem, generateRandomString, parseCommentBlock } from '@/utils' 5 | 6 | export interface ScriptState { 7 | name: string 8 | description: string 9 | tags: string[] 10 | } 11 | 12 | export type Feature = Parameters[0] 13 | 14 | export const useScriptStore = defineStore('script', { 15 | state: (): ScriptState => ({ 16 | name: '', 17 | description: '', 18 | tags: [] 19 | }), 20 | actions: { 21 | newScript(code: string) { 22 | const scriptId = generateRandomString(8) 23 | 24 | setItem(`script/${scriptId}`, { 25 | name: this.name, 26 | description: this.description, 27 | code 28 | }) 29 | 30 | Message.success('保存成功') 31 | 32 | return true 33 | }, 34 | 35 | parseCommentBlock(code: string) { 36 | const commentInfo = parseCommentBlock(code) 37 | this.$patch(commentInfo || {}) 38 | } 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /src/store/useSettingStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { setThemeMode } from '@/hooks/usePrefersTheme' 3 | import { setItem, getItem, removeItem, allDocs } from '@/utils' 4 | import { monaco } from '@/common/monaco' 5 | import { Message } from '@arco-design/web-vue' 6 | 7 | export type Theme = 'default' | 'dark' | 'light' 8 | export interface Snippet { 9 | /** 10 | * id (timestamp string) 11 | */ 12 | id: string 13 | /** 14 | * name 15 | */ 16 | label: string 17 | /** 18 | * content 19 | */ 20 | insertText: string 21 | /** 22 | * description 23 | */ 24 | documentation: string 25 | } 26 | 27 | export interface SettingStoreState { 28 | theme: Theme 29 | snippets: Snippet[] 30 | codeChangeDebounce?: number 31 | indentType: 'space' | 'tab' 32 | indentSize: number 33 | } 34 | 35 | export const useSettingStore = defineStore('setting', { 36 | state: (): SettingStoreState => ({ 37 | theme: getItem('setting/theme') || 'default', 38 | snippets: [], 39 | codeChangeDebounce: getItem('setting/codeChangeDebounce') || 300, 40 | indentType: getItem('setting/indentType') || 'space', 41 | indentSize: getItem('setting/indentSize') || 2 42 | }), 43 | 44 | actions: { 45 | init() { 46 | this.setTheme(this.theme) 47 | this.snippets = this.loadSnippets() 48 | 49 | monaco.languages.registerCompletionItemProvider('javascript', { 50 | // @ts-expect-error 51 | provideCompletionItems: () => { 52 | return { 53 | suggestions: this.snippets.map((item) => ({ 54 | ...item, 55 | kind: monaco.languages.CompletionItemKind.Snippet, 56 | insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet 57 | })) 58 | } 59 | } 60 | }) 61 | }, 62 | 63 | setTheme(theme: Theme) { 64 | this.theme = theme 65 | theme === 'default' ? removeItem('setting/theme') : setItem('setting/theme', theme) 66 | setThemeMode(theme) 67 | }, 68 | 69 | update(key: T) { 70 | return (value: SettingStoreState[T]) => { 71 | this.$state[key] = value 72 | setItem(`setting/${key}`, value) 73 | } 74 | }, 75 | 76 | loadSnippets() { 77 | const snippets = allDocs('setting/snippet') 78 | console.log('snippets', snippets) 79 | if (!snippets.length) 80 | return [ 81 | { 82 | id: new Date().getTime().toString(), 83 | label: 'clg', 84 | insertText: 'console.log(${1})', 85 | documentation: 'Print to Console' 86 | } 87 | ] 88 | 89 | return snippets.map((item) => ({ 90 | ...item.value, 91 | id: item._id.split('setting/snippet/')[1] 92 | })) 93 | }, 94 | 95 | newSnippet() { 96 | const snippet = { 97 | id: new Date().getTime().toString(), 98 | label: '', 99 | insertText: '', 100 | documentation: '' 101 | } 102 | this.snippets.push(snippet) 103 | Message.success('新建成功') 104 | }, 105 | 106 | removeSnippet(id: string) { 107 | this.snippets = this.snippets.filter((item) => item.id !== id) 108 | removeItem(`setting/snippet/${id}`) 109 | Message.success('删除成功') 110 | } 111 | } 112 | }) 113 | -------------------------------------------------------------------------------- /src/style/border.less: -------------------------------------------------------------------------------- 1 | .border-part(@position: top) { 2 | border-@{position}-width: 1px; 3 | border-@{position}-style: solid; 4 | border-@{position}-color: #e8e8e8; 5 | } 6 | 7 | .border() { 8 | border: 1px solid #e8e8e8; 9 | } 10 | -------------------------------------------------------------------------------- /src/style/index.less: -------------------------------------------------------------------------------- 1 | @import '@/style/transition.less'; 2 | 3 | body, 4 | html { 5 | margin: 0; 6 | padding: 0; 7 | color: var(--text-color); 8 | background-color: var(--bg-color); 9 | } 10 | 11 | body { 12 | --primary-color: #3271ae; 13 | --primary-color-lighter: #4997e1; 14 | --text-color: #333; 15 | --text-color-lighter: #d0d2d6; 16 | --text-bg-color: #e8e8e8; 17 | --text-bg-color-lighter: #f3f3f3; 18 | --nav-bg-color: linear-gradient(307deg, #ece9e6, #ffffff, #ece9e6); 19 | --nav-item-bg-color: #ebeaea; 20 | --bg-color: #fff; 21 | } 22 | 23 | body.dark { 24 | --primary-color: #448bd2; 25 | --primary-color-lighter: #4997e1; 26 | --text-color: #ffffff; 27 | --text-color-lighter: #efefef; 28 | --text-bg-color: #353535; 29 | --text-bg-color-lighter: #252525; 30 | --nav-bg-color: linear-gradient(130deg, #000000, #434343, #000000); 31 | --nav-item-bg-color: #2e2e2e; 32 | --bg-color: #1b1b1b; 33 | } 34 | -------------------------------------------------------------------------------- /src/style/scrollbar.less: -------------------------------------------------------------------------------- 1 | .scrollbar() { 2 | &::-webkit-scrollbar { 3 | width: 8px; 4 | height: 8px; 5 | background-color: var(--bg-color); 6 | } 7 | &::-webkit-scrollbar-thumb { 8 | background-color: var(--text-bg-color-lighter); 9 | border-radius: 2px; 10 | } 11 | &::-webkit-scrollbar-thumb:hover { 12 | background-color: var(--text-bg-color); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/style/transition.less: -------------------------------------------------------------------------------- 1 | .emerge-enter-active, 2 | .emerge-leave-active { 3 | transition: all 0.2s; 4 | } 5 | 6 | .emerge-enter-from, 7 | .emerge-leave-to { 8 | opacity: 0; 9 | transform: translateY(5px); 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/business.ts: -------------------------------------------------------------------------------- 1 | import { cloneDeep, concat } from 'lodash-es' 2 | import { parse } from 'comment-parser' 3 | import { isElectron } from './env' 4 | 5 | const uToolsLite = getuToolsLite() 6 | 7 | export async function runCodeInSandbox( 8 | code: string, 9 | callback: (log?: any, error?: any, warn?: any, info?: any) => any 10 | ) { 11 | const { electron, Buffer, require, vm } = await import('@/preload') 12 | const context = { 13 | console: consoleWithCallback(callback), 14 | fetch: fetch.bind(window), 15 | utools: uToolsLite, 16 | electron, 17 | Buffer, 18 | require 19 | } 20 | try { 21 | vm.createContext(context) 22 | vm.runInContext( 23 | `(async function(){ 24 | ${code} 25 | })()`, 26 | context 27 | ).catch((error: any) => { 28 | callback && callback(null, [error]) 29 | }) 30 | } catch (error: any) { 31 | callback && callback(null, [error]) 32 | } 33 | } 34 | 35 | export function runCodeInBrowser( 36 | code: string, 37 | callback: (log?: any, error?: any, warn?: any, info?: any) => any 38 | ) { 39 | try { 40 | const fn = new Function( 41 | 'console', 42 | 'utools', 43 | 'globalThis', 44 | `(async function(){\ntry{ 45 | ${code} 46 | }catch(error){console.error(error)}\n})()` 47 | ) 48 | const _globalThis = Object.assign({}, globalThis) 49 | // @ts-ignore 50 | _globalThis.utools = uToolsLite 51 | fn(consoleWithCallback(callback), uToolsLite, _globalThis) 52 | } catch (error: any) { 53 | callback && callback(null, [error]) 54 | } 55 | } 56 | 57 | export function consoleWithCallback(callback: any) { 58 | return { 59 | log: (...args: any[]) => callback && callback(args), 60 | error: (...args: any[]) => callback && callback(null, args), 61 | warn: (...args: any[]) => callback && callback(null, null, args), 62 | info: (...args: any[]) => callback && callback(null, null, null, args) 63 | } 64 | } 65 | 66 | export function getuToolsLite() { 67 | if (!isElectron) return {} 68 | const utoolsLite = Object.assign({}, cloneDeep(utools)) 69 | const dbBlackList = ['db', 'dbStorage', 'removeFeature', 'setFeature', 'onDbPull'] 70 | const payBlackList = ['fetchUserServerTemporaryToken', 'openPayment', 'fetchUserPayments'] 71 | const etcBlackList = ['onPluginEnter', 'onPluginOut', 'createBrowserWindow'] 72 | concat(dbBlackList, payBlackList, etcBlackList).forEach((item) => { 73 | // @ts-ignore 74 | delete utoolsLite[item] 75 | }) 76 | Object.freeze(utoolsLite) 77 | return utoolsLite 78 | } 79 | 80 | export function parseCommentBlock(code: string) { 81 | const parsed = parse(code) 82 | if (parsed.length) { 83 | // 只解析第一个注释块 首行默认作为 name 84 | // name 优先从首行获取 否则从 @name 获取 85 | // description 从 @description 获取 86 | const block = parsed[0] 87 | const commentInfo = block.tags.length 88 | ? block.tags.map((tag) => ({ 89 | name: block.description 90 | ? block.description 91 | : tag.tag === 'name' 92 | ? [tag.name, tag.description].join(' ') 93 | : '', 94 | description: tag.tag === 'description' ? [tag.name, tag.description].join(' ') : '' 95 | }))[0] 96 | : { 97 | name: block.description || '' 98 | } 99 | return commentInfo 100 | } 101 | return null 102 | } 103 | -------------------------------------------------------------------------------- /src/utils/env.ts: -------------------------------------------------------------------------------- 1 | export const isElectron = window && typeof window?.utools !== 'undefined' 2 | export const SERVER_DEV = import.meta.env.DEV 3 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './storage' 2 | export * from './tools' 3 | export * from './env' 4 | export * from './utools' 5 | export * from './business' 6 | -------------------------------------------------------------------------------- /src/utils/storage.ts: -------------------------------------------------------------------------------- 1 | import { isElectron } from '@/utils' 2 | 3 | export function setItem(key: string, value: any) { 4 | const data = JSON.stringify(value) 5 | 6 | if (isElectron) { 7 | return utools.dbStorage.setItem(key, data) 8 | } else { 9 | localStorage.setItem(key, data) 10 | } 11 | } 12 | 13 | export function getItem(key: string) { 14 | if (isElectron) { 15 | const data = utools.dbStorage.getItem(key) 16 | return data ? JSON.parse(data) : null 17 | } else { 18 | const data = localStorage.getItem(key) 19 | return data ? JSON.parse(data) : null 20 | } 21 | } 22 | 23 | export function removeItem(key: string) { 24 | const dbStorage = isElectron ? utools.dbStorage : localStorage 25 | dbStorage.removeItem(key) 26 | } 27 | 28 | export function allDocs(key?: string) { 29 | if (isElectron) { 30 | return utools.db.allDocs(key).map((db) => ({ 31 | ...db, 32 | value: JSON.parse(db.value) 33 | })) 34 | } 35 | 36 | const res: { [x: string]: any; _id: string }[] = [] 37 | 38 | if (!localStorage.length) { 39 | return res 40 | } 41 | 42 | for (let i = 0; i < localStorage.length; i++) { 43 | const _id = localStorage.key(i) 44 | 45 | // filter by key if key is provided 46 | if (key && _id && !_id.startsWith(key)) { 47 | continue 48 | } 49 | 50 | if (_id) { 51 | const data = localStorage.getItem(_id) 52 | res.push({ 53 | value: data ? JSON.parse(data) : null, 54 | _id 55 | }) 56 | } 57 | } 58 | 59 | return res 60 | } 61 | -------------------------------------------------------------------------------- /src/utils/tools.ts: -------------------------------------------------------------------------------- 1 | export function classof(o: any) { 2 | if (o === null) return 'Null' 3 | if (o === undefined) return 'Undefined' 4 | return Object.prototype.toString.call(o).slice(8, -1) 5 | } 6 | 7 | export function formatTime(timeStamp: number) { 8 | // example: 2021年8月12日 08:08:08 9 | const date = new Date(timeStamp) 10 | const year = date.getFullYear() 11 | const month = date.getMonth() + 1 12 | const day = date.getDate() 13 | const hour = date.getHours() 14 | const minute = date.getMinutes() 15 | const second = date.getSeconds() 16 | 17 | const formatNumber = (n: number) => { 18 | const sn = n.toString() 19 | return sn[1] ? sn : '0' + sn 20 | } 21 | 22 | return `${year}年${formatNumber(month)}月${formatNumber(day)}日 ${formatNumber( 23 | hour 24 | )}:${formatNumber(minute)}:${formatNumber(second)}` 25 | } 26 | 27 | export function copyText(text: string) { 28 | try { 29 | if (navigator.clipboard) { 30 | navigator.clipboard.writeText(text) 31 | } else { 32 | var textarea = document.createElement('textarea') 33 | document.body.appendChild(textarea) 34 | textarea.style.position = 'fixed' 35 | textarea.style.clip = 'rect(0 0 0 0)' 36 | textarea.style.top = '10px' 37 | textarea.value = text 38 | textarea.select() 39 | document.execCommand('copy', true) 40 | document.body.removeChild(textarea) 41 | } 42 | return true 43 | } catch (err) { 44 | console.error(err) 45 | return false 46 | } 47 | } 48 | 49 | export function generateRandomString(length: number) { 50 | let result = '' 51 | const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' 52 | const charactersLength = characters.length 53 | for (let i = 0; i < length; i++) { 54 | result += characters.charAt(Math.floor(Math.random() * charactersLength)) 55 | } 56 | return result 57 | } 58 | -------------------------------------------------------------------------------- /src/utils/utools.ts: -------------------------------------------------------------------------------- 1 | import { Message } from '@arco-design/web-vue' 2 | import { isElectron } from './env' 3 | 4 | export function openLink(url: string) { 5 | return isElectron ? utools.shellOpenExternal(url) : window.open(url) 6 | } 7 | 8 | export function pickPicture(options: { isMultiple?: boolean } = {}) { 9 | const extensions = ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'webp', 'svg', 'ico'] 10 | 11 | if (isElectron) { 12 | return new Promise((resolve) => { 13 | resolve(utools.showOpenDialog({ filters: [{ name: '', extensions }] }) || undefined) 14 | }) 15 | } 16 | 17 | return new Promise((resolve) => { 18 | const input = document.createElement('input') 19 | input.type = 'file' 20 | input.multiple = !!options.isMultiple 21 | input.accept = extensions.map((ext) => `image/${ext}`).join(',') || '' 22 | input.click() 23 | input.onchange = () => { 24 | const files = input.files || undefined 25 | if (!files) return 26 | 27 | // 检查文件类型 28 | for (let i = 0; i < files.length; i++) { 29 | const file = files?.item(i) 30 | if (file && !extensions.includes(file.name.split('.').pop()?.toLowerCase() || '')) { 31 | Message.error('文件类型不符合要求') 32 | return 33 | } 34 | } 35 | resolve(input.files || undefined) 36 | } 37 | input.onabort = () => { 38 | resolve(undefined) 39 | } 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /src/views/About.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 36 | 37 | 78 | -------------------------------------------------------------------------------- /src/views/Runner.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 122 | 123 | 142 | -------------------------------------------------------------------------------- /src/views/Setting.vue: -------------------------------------------------------------------------------- 1 | 84 | 85 | 100 | 101 | 107 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "lib": ["ESNext", "DOM"], 13 | "skipLibCheck": true, 14 | "noEmit": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "@/*": ["src/*"] 18 | }, 19 | "types": ["utools-api-types", "node"] 20 | }, 21 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "auto-imports.d.ts"], 22 | "references": [{ "path": "./tsconfig.node.json" }] 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts", "build/**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs' 3 | import process from 'process' 4 | import { defineConfig } from 'vite' 5 | import vue from '@vitejs/plugin-vue' 6 | import AutoImport from 'unplugin-auto-import/vite' 7 | import Components from 'unplugin-vue-components/vite' 8 | import { ArcoResolver } from 'unplugin-vue-components/resolvers' 9 | import { createStyleImportPlugin } from 'vite-plugin-style-import' 10 | import postBuildPlugin from './build/postBuildPlugin' 11 | 12 | export default defineConfig({ 13 | base: './', 14 | resolve: { 15 | alias: { 16 | '@': '/src' 17 | }, 18 | extensions: ['.ts', '.tsx', '.js', '.jsx', '.json', '.vue'] 19 | }, 20 | optimizeDeps: { 21 | entries: ['monaco-editor', '@arco-design/web-vue'], 22 | needsInterop: [ 23 | 'monaco-editor/esm/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp', 24 | 'monaco-editor/esm/vs/editor/contrib/documentSymbols/browser/documentSymbols', 25 | 'monaco-editor/esm/vs/editor/contrib/format/browser/formatActions, monaco-editor/esm/vs/editor/contrib/inPlaceReplace/browser/inPlaceReplace', 26 | 'monaco-editor/esm/vs/editor/standalone/browser/inspectTokens/inspectTokens' 27 | ] 28 | }, 29 | build: { 30 | rollupOptions: { 31 | output: { 32 | manualChunks: (e) => { 33 | if (e.includes('/node_modules/monaco-editor/')) return 'monaco' 34 | return 'vendor' 35 | } 36 | }, 37 | external: ['electron', 'utools', 'process', 'vm', 'fs'] 38 | } 39 | }, 40 | esbuild: { 41 | drop: process.env.NODE_ENV === 'production' ? ['console'] : [] 42 | }, 43 | server: { 44 | port: 8084 45 | }, 46 | plugins: [ 47 | vue({ 48 | script: { 49 | defineModel: true 50 | } 51 | }), 52 | AutoImport({ 53 | imports: ['vue', 'vue-router', 'pinia'], 54 | resolvers: [ArcoResolver()] 55 | }), 56 | Components({ 57 | resolvers: [ 58 | ArcoResolver({ 59 | sideEffect: true 60 | }) 61 | ] 62 | }), 63 | createStyleImportPlugin({ 64 | libs: [ 65 | { 66 | libraryName: '@arco-design/web-vue', 67 | esModule: true, 68 | resolveStyle: (name) => { 69 | const p = `@arco-design/web-vue/es/${name}/style/css.js` 70 | const actualPath = path.resolve(__dirname, 'node_modules', p) 71 | if (fs.existsSync(actualPath)) return p 72 | return '' 73 | } 74 | } 75 | ] 76 | }), 77 | postBuildPlugin({ 78 | files: ['index.html', 'plugin.json', 'preload.js'] 79 | }) 80 | ] 81 | }) 82 | --------------------------------------------------------------------------------