├── .gitignore ├── IP Recorder.fig ├── README.md ├── build ├── app-icons │ ├── mac │ │ └── icon.icns │ ├── png │ │ ├── 1024x1024.png │ │ ├── 128x128.png │ │ ├── 16x16.png │ │ ├── 24x24.png │ │ ├── 256x256.png │ │ ├── 32x32.png │ │ ├── 48x48.png │ │ ├── 512x512.png │ │ └── 64x64.png │ └── win │ │ └── icon.ico ├── icon.png └── tray-icons │ ├── linux │ └── tray-icon.png │ ├── mac │ ├── tray-icon.png │ └── tray-icon@2x.png │ └── win │ └── tray-icon.ico ├── dev-app-update.yml ├── electron-builder.yml ├── electron.vite.config.mjs ├── package-lock.json ├── package.json ├── resources ├── app-icons │ ├── mac │ │ └── icon.icns │ ├── png │ │ ├── 1024x1024.png │ │ ├── 128x128.png │ │ ├── 16x16.png │ │ ├── 256x256.png │ │ ├── 32x32.png │ │ ├── 48x48.png │ │ ├── 512x512.png │ │ └── 64x64.png │ └── win │ │ └── icon.ico ├── icon.png └── tray-icons │ ├── linux │ └── tray-icon.png │ ├── mac │ ├── tray-icon.png │ └── tray-icon@2x.png │ └── win │ └── tray-icon.ico ├── src ├── main │ ├── controllers │ │ └── recorderController.mjs │ ├── index.mjs │ ├── ipc │ │ └── handlers │ │ │ ├── apiSourceHandlers.mjs │ │ │ ├── proxyHandlers.mjs │ │ │ ├── recordHandlers.mjs │ │ │ ├── requestOptionsHandlers.mjs │ │ │ └── settingsHandlers.mjs │ ├── services │ │ ├── Recorder.mjs │ │ └── ipService.mjs │ ├── stores │ │ ├── configStore.mjs │ │ └── recorderStore.mjs │ ├── system │ │ ├── ipcSetup.mjs │ │ └── tray.mjs │ ├── utils │ │ ├── debugConsole.mjs │ │ ├── exportToCsv.mjs │ │ ├── iconUtils.mjs │ │ ├── paths.mjs │ │ ├── proxyWapper.mjs │ │ ├── response.mjs │ │ └── times.mjs │ └── windows │ │ ├── commonWindow.mjs │ │ ├── mainWindow.mjs │ │ └── settingsWindow.mjs ├── preload │ ├── main │ │ ├── api │ │ │ ├── ipHandler.js │ │ │ ├── recordControl.js │ │ │ └── windowControl.js │ │ ├── index-with-tear.js │ │ └── index.js │ └── settings │ │ ├── api │ │ ├── apiSourceControl.js │ │ ├── proxyControl.js │ │ ├── requestOptionsControl.js │ │ └── windowControl.js │ │ ├── index-with-tear.js │ │ └── index.js └── renderer │ ├── auto-imports.d.ts │ ├── components.d.ts │ └── src │ ├── assets │ ├── image │ │ ├── api-select.png │ │ ├── api.png │ │ ├── baidu-logo.png │ │ ├── baidu.png │ │ ├── close.png │ │ ├── down.png │ │ ├── export.png │ │ ├── ip-api-logo.png │ │ ├── ip-api.png │ │ ├── ipify-logo.png │ │ ├── ipify.png │ │ ├── ipinfo.png │ │ ├── ipinfo.svg │ │ ├── ipipnet-logo.png │ │ ├── ipipnet.png │ │ ├── line-l.png │ │ ├── line-s.png │ │ ├── meitu.png │ │ ├── minimize.png │ │ ├── pause-dark.png │ │ ├── pause.png │ │ ├── proxy.png │ │ ├── radio-selected.png │ │ ├── radio.png │ │ ├── request.png │ │ ├── select-button.png │ │ ├── selected-button.png │ │ ├── settings.png │ │ ├── start-dark.png │ │ ├── start.png │ │ ├── stop-dark.png │ │ ├── stop-enabled-dark.png │ │ ├── stop-enabled.png │ │ ├── stop.png │ │ ├── theme-dark.png │ │ ├── theme-round.png │ │ ├── theme.png │ │ └── up.png │ └── styles │ │ ├── _mixins.scss │ │ ├── _variables.scss │ │ ├── base.scss │ │ ├── index.scss │ │ └── theme.scss │ ├── components │ ├── main │ │ └── layout │ │ │ ├── ActionBtn.vue │ │ │ ├── Header.vue │ │ │ ├── IPInfo.vue │ │ │ └── ToolBar.vue │ ├── settings │ │ ├── contents │ │ │ ├── ApiSource.vue │ │ │ ├── Proxy.vue │ │ │ ├── RequestOpt.vue │ │ │ ├── Theme.vue │ │ │ └── apiCard │ │ │ │ └── apiCards.vue │ │ ├── layout │ │ │ ├── Content.vue │ │ │ └── DragBar.vue │ │ └── utils │ │ │ ├── BtnGroup.vue │ │ │ ├── SaveBtn.vue │ │ │ └── TestBtn.vue │ └── utils │ │ ├── ComfirmBox.vue │ │ └── MessageBox.vue │ ├── main │ ├── App.vue │ ├── main.html │ └── main.js │ ├── settings │ ├── Settings.vue │ ├── settings.html │ └── settings.js │ ├── stores │ └── theme.js │ └── utils │ └── useMessage.js └── 接口文档.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /IP Recorder.fig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/IP Recorder.fig -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 3 | 大家好,我们是 **NOP Team**,我们是一家做安全服务的公司,平时在做渗透测试工作的过程中,经常会遇到一个问题,工作场景IP地址不固定,相信很多朋友也遇到,尤其是出差等场景。 4 | 5 | 常规情况下,渗透测试工作开始前需要在授权委托书中写明本次测试使用的IP地址,但是如果IP地址可能会变化,那么可能要求会放宽,也就是渗透测试结束后,提交在此期间使用过的IP地址 6 | 7 | 最近在练习 Electron 开发,于是开源了一款工具 —— `IP Recorder` 来解决这个问题 8 | 9 | 10 |
11 | 12 | ## 下载地址 13 | 14 | **Github** 15 | 16 | > https://github.com/Just-Hack-For-Fun/IP-Recorder 17 | 18 | 19 | **百度云盘** 20 | 21 | > https://pan.baidu.com/s/1oDTbTX1XWvsJ8TqSfJ46dQ?pwd=9vn7 22 | 23 | 24 |
25 | 26 | ## 使用方法 27 | 28 | 程序主页面如下: 29 | 30 | image-20250104025351265 31 | 32 | 功能比较直观,左侧显示IP信息,右侧是记录的控制按钮,最右侧为工具栏,可以点击设置按钮进入设置页面 33 | 34 | image-20250104025604369 35 | 36 | 37 | 38 | 设置页面可以进行相关配置 39 | 40 | 本程序记录IP地址以及IP归属地使用的接口如下 41 | 42 | ``` 43 | https://myip.ipip.net/json 44 | https://api.ipify.org/?format=json // 使用 https://ip.taobao.com 获取IP归属地 45 | https://qifu-api.baidubce.com/ip/local/geo/v1/district 46 | http://demo.ip-api.com/json/?lang=zh-CN 47 | ``` 48 | 49 | 50 |
51 | 52 | ## 手工编译 53 | 54 | 如果大家希望手工编译,也非常简单,安装 `Node.js` 最新版,下载源代码,在源代码根目录执行以下命令 55 | 56 | ```bash 57 | npm i 58 | npm run build:mac # 编译 MacOS 版本 59 | npm run build:win # 编译 Windows 版本 60 | npm run build:linux # 编译 Linux 版本 61 | ``` 62 | 63 | 如果出现网络错误,可以考虑设置 npm 国内源 64 | 65 | ![image-20250104030222919](http://mweb-tc.oss-cn-beijing.aliyuncs.com/2025-01-03-190223.png) 66 | 67 | 之后在 dist 目录下就生成了打包好的程序 68 | 69 | ![image-20250104030249564](http://mweb-tc.oss-cn-beijing.aliyuncs.com/2025-01-03-190249.png) 70 | 71 | -------------------------------------------------------------------------------- /build/app-icons/mac/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/build/app-icons/mac/icon.icns -------------------------------------------------------------------------------- /build/app-icons/png/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/build/app-icons/png/1024x1024.png -------------------------------------------------------------------------------- /build/app-icons/png/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/build/app-icons/png/128x128.png -------------------------------------------------------------------------------- /build/app-icons/png/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/build/app-icons/png/16x16.png -------------------------------------------------------------------------------- /build/app-icons/png/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/build/app-icons/png/24x24.png -------------------------------------------------------------------------------- /build/app-icons/png/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/build/app-icons/png/256x256.png -------------------------------------------------------------------------------- /build/app-icons/png/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/build/app-icons/png/32x32.png -------------------------------------------------------------------------------- /build/app-icons/png/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/build/app-icons/png/48x48.png -------------------------------------------------------------------------------- /build/app-icons/png/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/build/app-icons/png/512x512.png -------------------------------------------------------------------------------- /build/app-icons/png/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/build/app-icons/png/64x64.png -------------------------------------------------------------------------------- /build/app-icons/win/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/build/app-icons/win/icon.ico -------------------------------------------------------------------------------- /build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/build/icon.png -------------------------------------------------------------------------------- /build/tray-icons/linux/tray-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/build/tray-icons/linux/tray-icon.png -------------------------------------------------------------------------------- /build/tray-icons/mac/tray-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/build/tray-icons/mac/tray-icon.png -------------------------------------------------------------------------------- /build/tray-icons/mac/tray-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/build/tray-icons/mac/tray-icon@2x.png -------------------------------------------------------------------------------- /build/tray-icons/win/tray-icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/build/tray-icons/win/tray-icon.ico -------------------------------------------------------------------------------- /dev-app-update.yml: -------------------------------------------------------------------------------- 1 | provider: generic 2 | url: https://example.com/auto-updates 3 | updaterCacheDirName: ip-recorder-updater 4 | -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | appId: com.electron.app 2 | productName: ip-recorder 3 | directories: 4 | buildResources: build 5 | files: 6 | - '!**/.vscode/*' 7 | - '!src/*' 8 | - '!electron.vite.config.{js,ts,mjs,cjs}' 9 | - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' 10 | - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' 11 | asarUnpack: 12 | - resources/** 13 | win: 14 | executableName: ip-recorder 15 | nsis: 16 | artifactName: ${name}-${version}-setup.${ext} 17 | shortcutName: ${productName} 18 | uninstallDisplayName: ${productName} 19 | createDesktopShortcut: always 20 | mac: 21 | entitlementsInherit: build/entitlements.mac.plist 22 | extendInfo: 23 | - NSCameraUsageDescription: Application requests access to the device's camera. 24 | - NSMicrophoneUsageDescription: Application requests access to the device's microphone. 25 | - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. 26 | - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. 27 | notarize: false 28 | dmg: 29 | artifactName: ${name}-${version}.${ext} 30 | linux: 31 | target: 32 | - AppImage 33 | - snap 34 | - deb 35 | maintainer: electronjs.org 36 | category: Utility 37 | appImage: 38 | artifactName: ${name}-${version}.${ext} 39 | npmRebuild: false 40 | publish: 41 | provider: generic 42 | url: https://example.com/auto-updates 43 | electronDownload: 44 | mirror: https://npmmirror.com/mirrors/electron/ 45 | -------------------------------------------------------------------------------- /electron.vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite' 3 | import vue from '@vitejs/plugin-vue' 4 | import AutoImport from 'unplugin-auto-import/vite' 5 | import Components from 'unplugin-vue-components/vite' 6 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' 7 | 8 | export default defineConfig({ 9 | main: { 10 | plugins: [externalizeDepsPlugin()], 11 | build: { // build 应该是顶级配置,不是在 resolve 里面 12 | rollupOptions: { 13 | input: { 14 | index: resolve(__dirname, 'src/main/index.mjs') // 使用 resolve 来确保路径正确 15 | }, 16 | external: ['electron'], 17 | output: { 18 | format: 'es', 19 | entryFileNames: '[name].mjs' 20 | } 21 | } 22 | }, 23 | resolve: { 24 | alias: { 25 | '@main': resolve('src/main/'), 26 | '@mainPreload': resolve('src/preload/main'), 27 | '@settingsPreload': resolve('src/preload/settings'), 28 | '@mainRenderer': resolve('src/renderer/src/main/'), 29 | '@settingsRenderer': resolve('src/renderer/src/settings/') 30 | } 31 | } 32 | }, 33 | preload: { 34 | plugins: [externalizeDepsPlugin()], 35 | build: { 36 | rollupOptions: { 37 | input: { 38 | 'main/index': resolve(__dirname, 'src/preload/main/index.js'), 39 | 'settings/index': resolve(__dirname, 'src/preload/settings/index.js') 40 | }, 41 | external: ['electron'], 42 | output: { 43 | format: 'cjs', 44 | entryFileNames: '[name].js' 45 | } 46 | } 47 | }, 48 | resolve: { 49 | alias: { 50 | '@preload': resolve('src/preload'), 51 | '@main': resolve('src/preload/main'), 52 | '@settings': resolve('src/preload/settings') 53 | } 54 | } 55 | }, 56 | renderer: { 57 | resolve: { 58 | alias: { 59 | '@renderer': resolve('src/renderer/src'), 60 | '@image': resolve('src/renderer/src/assets/image'), 61 | '@mainLayout': resolve('src/renderer/src/components/main/layout'), 62 | '@setLayout': resolve('src/renderer/src/components/settings/layout'), 63 | '@styles': resolve('src/renderer/src/assets/styles'), 64 | '@components': resolve('src/renderer/src/components/'), 65 | '@stores': resolve('src/renderer/src/stores') 66 | } 67 | }, 68 | plugins: [ 69 | vue(), 70 | AutoImport({ 71 | resolvers: [ElementPlusResolver()] 72 | }), 73 | Components({ 74 | resolvers: [ElementPlusResolver()] 75 | }) 76 | ], 77 | build: { 78 | rollupOptions: { 79 | input: { 80 | main: resolve(__dirname, 'src/renderer/src/main/main.html'), 81 | settings: resolve(__dirname, 'src/renderer/src/settings/settings.html') 82 | } 83 | } 84 | }, 85 | css: { 86 | preprocessorOptions: { 87 | scss: { 88 | // 使用 @use 替代 @import 89 | additionalData: `@use "@styles/index.scss" as *;`, 90 | api: 'modern-compiler' 91 | // 临时禁用警告 92 | // sassOptions: { 93 | // silenceDeprecations: ['legacy-js-api', 'import'] 94 | // } 95 | } 96 | } 97 | } 98 | } 99 | }) 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "IP-Recorder", 3 | "version": "1.2.0", 4 | "description": "一个记录本机IP地址的程序", 5 | "main": "./out/main/index.mjs", 6 | "author": "NOP Team", 7 | "homepage": "https://github.com/Just-Hack-For-Fun/IP-Recorder/", 8 | "scripts": { 9 | "format": "prettier --write .", 10 | "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", 11 | "start": "electron-vite preview", 12 | "dev": "electron-vite dev --watch", 13 | "build": "electron-vite build", 14 | "postinstall": "electron-builder install-app-deps", 15 | "build:unpack": "npm run build && electron-builder --dir", 16 | "build:win": "npm run build && electron-builder --win", 17 | "build:mac": "npm run build && electron-builder --mac", 18 | "build:linux": "npm run build && electron-builder --linux" 19 | }, 20 | "dependencies": { 21 | "@electron-toolkit/preload": "^3.0.1", 22 | "@electron-toolkit/utils": "^3.0.0", 23 | "axios": "^1.7.9", 24 | "electron-store": "^10.0.0", 25 | "electron-updater": "^6.1.7", 26 | "http-proxy-agent": "^7.0.2", 27 | "https-proxy-agent": "^7.0.6", 28 | "pinia": "^2.3.0", 29 | "pinia-plugin-persistedstate": "^4.2.0", 30 | "socks-proxy-agent": "^8.0.5" 31 | }, 32 | "devDependencies": { 33 | "@electron-toolkit/eslint-config": "^1.0.2", 34 | "@rushstack/eslint-patch": "^1.10.3", 35 | "@vitejs/plugin-vue": "^5.0.5", 36 | "@vue/eslint-config-prettier": "^9.0.0", 37 | "electron": "^31.0.2", 38 | "electron-builder": "^24.13.3", 39 | "electron-vite": "^2.3.0", 40 | "eslint": "^8.57.0", 41 | "eslint-plugin-vue": "^9.26.0", 42 | "prettier": "^3.3.2", 43 | "sass": "^1.83.0", 44 | "unplugin-auto-import": "^0.19.0", 45 | "unplugin-vue-components": "^0.28.0", 46 | "vite": "^5.3.1", 47 | "vue": "^3.4.30" 48 | }, 49 | "build": { 50 | "appId": "com.ip-recorder.id", 51 | "productName": "IP-Recorder" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /resources/app-icons/mac/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/resources/app-icons/mac/icon.icns -------------------------------------------------------------------------------- /resources/app-icons/png/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/resources/app-icons/png/1024x1024.png -------------------------------------------------------------------------------- /resources/app-icons/png/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/resources/app-icons/png/128x128.png -------------------------------------------------------------------------------- /resources/app-icons/png/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/resources/app-icons/png/16x16.png -------------------------------------------------------------------------------- /resources/app-icons/png/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/resources/app-icons/png/256x256.png -------------------------------------------------------------------------------- /resources/app-icons/png/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/resources/app-icons/png/32x32.png -------------------------------------------------------------------------------- /resources/app-icons/png/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/resources/app-icons/png/48x48.png -------------------------------------------------------------------------------- /resources/app-icons/png/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/resources/app-icons/png/512x512.png -------------------------------------------------------------------------------- /resources/app-icons/png/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/resources/app-icons/png/64x64.png -------------------------------------------------------------------------------- /resources/app-icons/win/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/resources/app-icons/win/icon.ico -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/resources/icon.png -------------------------------------------------------------------------------- /resources/tray-icons/linux/tray-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/resources/tray-icons/linux/tray-icon.png -------------------------------------------------------------------------------- /resources/tray-icons/mac/tray-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/resources/tray-icons/mac/tray-icon.png -------------------------------------------------------------------------------- /resources/tray-icons/mac/tray-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/resources/tray-icons/mac/tray-icon@2x.png -------------------------------------------------------------------------------- /resources/tray-icons/win/tray-icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/resources/tray-icons/win/tray-icon.ico -------------------------------------------------------------------------------- /src/main/controllers/recorderController.mjs: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron' 2 | import recorder from '../services/Recorder.mjs' 3 | import { getConfig } from '../stores/configStore.mjs' 4 | import { print } from '../utils/debugConsole.mjs' 5 | import { response } from '../utils/response.mjs' 6 | 7 | 8 | // 用于注册 record 相关 ipc 通信的类 9 | class RecorderController { 10 | // 私有属性 11 | #handlers 12 | #registeredChannels = new Set() 13 | 14 | constructor() { 15 | // IPC 处理器映射表 16 | this.#handlers = { 17 | 'get-recorder-status': () => recorder.getStatus(), 18 | 'start-record': async () => { 19 | const config = getConfig() 20 | return await recorder.start(config) 21 | }, 22 | 'pause-record': async () => recorder.pause(), 23 | 'resume-record': async () => recorder.resume(), 24 | 'stop-record': async () => recorder.stop() 25 | } 26 | 27 | this.#setupIPCHandlers() 28 | } 29 | 30 | #setupIPCHandlers() { 31 | try { 32 | for (const [channel, handler] of Object.entries(this.#handlers)) { 33 | this.#registerHandler(channel, handler) 34 | } 35 | // print('IPC handlers setup completed') 36 | } catch (error) { 37 | console.error('Failed to setup IPC handlers:', error) 38 | throw error 39 | } 40 | } 41 | 42 | #registerHandler(channel, handler) { 43 | if (this.#registeredChannels.has(channel)) { 44 | console.warn(`Handler for channel '${channel}' is already registered`) 45 | return 46 | } 47 | 48 | ipcMain.handle(channel, async (event, ...args) => { 49 | try { 50 | // print(`Handling ${channel} request`) 51 | const result = await handler(...args) 52 | // print(`${channel} request completed`) 53 | return result 54 | } catch (error) { 55 | console.error(`Error handling ${channel}:`, error) 56 | return response.error(`执行 ${channel} 时发生错误: ${error.message}`) 57 | } 58 | }) 59 | 60 | this.#registeredChannels.add(channel) 61 | } 62 | 63 | init() { 64 | try { 65 | // print('Recorder controller initialized successfully') 66 | } catch (error) { 67 | console.error('Failed to initialize recorder controller:', error) 68 | throw error 69 | } 70 | } 71 | 72 | destroy() { 73 | try { 74 | // print('Cleaning up recorder controller') 75 | // 移除所有注册的 IPC 处理器 76 | for (const channel of this.#registeredChannels) { 77 | ipcMain.removeHandler(channel) 78 | } 79 | this.#registeredChannels.clear() 80 | 81 | recorder.destroy() 82 | // print('Recorder controller cleanup completed') 83 | } catch (error) { 84 | console.error('Error during recorder controller cleanup:', error) 85 | // 即使清理出错也继续执行 86 | } 87 | } 88 | 89 | // 用于测试的方法 90 | isHandlerRegistered(channel) { 91 | return this.#registeredChannels.has(channel) 92 | } 93 | } 94 | 95 | const recorderController = new RecorderController() 96 | export default recorderController 97 | -------------------------------------------------------------------------------- /src/main/index.mjs: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron' 2 | import { electronApp, optimizer } from '@electron-toolkit/utils' 3 | import { MainWindow } from './windows/mainWindow.mjs' 4 | import { TrayManager } from './system/tray.mjs' 5 | import { setupIPC } from './system/ipcSetup.mjs' 6 | import recorderController from './controllers/recorderController.mjs' 7 | import { print } from './utils/debugConsole.mjs' 8 | import { SettingsWindow } from './windows/settingsWindow.mjs' 9 | 10 | const initialize = () => { 11 | electronApp.setAppUserModelId('com.ip-recorder.id') 12 | 13 | app.on('browser-window-created', (_, window) => { 14 | optimizer.watchWindowShortcuts(window) 15 | }) 16 | 17 | setupIPC() 18 | recorderController.init() 19 | MainWindow.create() 20 | TrayManager.createTray() 21 | 22 | } 23 | 24 | const setupAppEvents = () => { 25 | app.on('activate', () => { 26 | if (BrowserWindow.getAllWindows().length === 0) { 27 | MainWindow.create() 28 | } 29 | }) 30 | 31 | app.on('window-all-closed', () => { 32 | if (process.platform !== 'darwin') { 33 | app.quit() 34 | } 35 | }) 36 | 37 | app.on('before-quit', async (event) => { 38 | event.preventDefault() 39 | try { 40 | recorderController.destroy() 41 | TrayManager.destroy() 42 | 43 | if (SettingsWindow?.isCreated()) { 44 | SettingsWindow?.destroy() 45 | } 46 | if (MainWindow?.isCreated()) { 47 | SettingsWindow.destroy() 48 | } 49 | app.exit(0) 50 | } catch (error) { 51 | console.error(error) 52 | app.exit(1) 53 | } 54 | 55 | }) 56 | 57 | process.on('uncaughtException', (error) => { 58 | console.error('未捕获的异常:', error) 59 | }) 60 | } 61 | 62 | app.whenReady() 63 | .then(() => { 64 | initialize() 65 | setupAppEvents() 66 | }) 67 | .catch(console.error) 68 | -------------------------------------------------------------------------------- /src/main/ipc/handlers/apiSourceHandlers.mjs: -------------------------------------------------------------------------------- 1 | import { getApiSource, setApiSource } from '../../stores/configStore.mjs' 2 | import { getInfoWithOpts } from '../../services/ipService.mjs' 3 | 4 | export const apiSourceHandlers = { 5 | getApiSource, 6 | testApiSource: async (_, apiSource) => await getInfoWithOpts({ apiSource }), 7 | saveApiSource: (_, api) => setApiSource(api) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/ipc/handlers/proxyHandlers.mjs: -------------------------------------------------------------------------------- 1 | import { getProxy, setProxy } from '../../stores/configStore.mjs' 2 | import { getInfoWithOpts } from '../../services/ipService.mjs' 3 | 4 | export const proxyHandlers = { 5 | getProxy, 6 | testProxy: async (_, proxy) => await getInfoWithOpts({ proxy }), 7 | saveProxy: (_, proxy) => setProxy(proxy) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/ipc/handlers/recordHandlers.mjs: -------------------------------------------------------------------------------- 1 | import { hasHistory, resetRecord } from '../../stores/recorderStore.mjs' 2 | import exportToCSV from '../../utils/exportToCsv.mjs' 3 | import { updateCurrentInfo } from '../../services/ipService.mjs' 4 | 5 | export const recordHandlers = { 6 | hasHistory, 7 | resetRecord, 8 | exportRecord: async () => await exportToCSV(), 9 | updateCurrentInfo 10 | } 11 | -------------------------------------------------------------------------------- /src/main/ipc/handlers/requestOptionsHandlers.mjs: -------------------------------------------------------------------------------- 1 | import { 2 | setRetryCounts, 3 | setReqInterval, 4 | getReqOptions 5 | } from '../../stores/configStore.mjs' 6 | // import { updateCurrentInfo } from '../../services/ipService.mjs' 7 | import { response } from '../../utils/response.mjs' 8 | 9 | export const requestOptionsHandlers = { 10 | getReqOptions, 11 | saveReqOptions: async (_, options) => { 12 | const result1 = await setRetryCounts(options.times) 13 | const result2 = await setReqInterval(options.interval) 14 | 15 | if (result1.code === 0 && result2.code === 0) { 16 | // updateCurrentInfo() 17 | return response.success() 18 | } 19 | return response.error(result1.message === 'success' ? result2.message : result1.message) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/ipc/handlers/settingsHandlers.mjs: -------------------------------------------------------------------------------- 1 | import { SettingsWindow } from '../../windows/settingsWindow.mjs' 2 | import { MainWindow } from '../../windows/mainWindow.mjs' 3 | import { print } from '../../utils/debugConsole.mjs' 4 | 5 | export const settingsHandlers = { 6 | openSetting: () => { 7 | SettingsWindow.createOrFocus() 8 | }, 9 | closeSetWin: () => SettingsWindow?.close(), 10 | minMainWindow: () => MainWindow.minimize(), 11 | closeMainWin: () => { 12 | SettingsWindow?.close() 13 | MainWindow.hide() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/services/Recorder.mjs: -------------------------------------------------------------------------------- 1 | import { addRecord } from '../stores/recorderStore.mjs' 2 | import { response } from '../utils/response.mjs' 3 | import { getInfoWithOpts } from './ipService.mjs' 4 | import { MainWindow } from '../windows/mainWindow.mjs' 5 | import { print } from '../utils/debugConsole.mjs' 6 | import { getInterval, subscribeIntervalChange } from '../stores/configStore.mjs' 7 | 8 | class Recorder { 9 | #timer = null 10 | #isRunning = false 11 | #currentRecord = null 12 | #lastError = null 13 | #interval = null 14 | #unsubscribe = null 15 | 16 | constructor() { 17 | this.#setupIntervalSubscription() 18 | } 19 | 20 | #setupIntervalSubscription() { 21 | this.#unsubscribe = subscribeIntervalChange((newConfig, oldConfig) => { 22 | if (this.#isRunning && newConfig.interval !== oldConfig.interval) { 23 | this.#updateInterval(newConfig.interval) 24 | } 25 | }) 26 | } 27 | 28 | #updateInterval(newInterval) { 29 | // print(`Updating check interval to ${newInterval}ms`) 30 | 31 | if (this.#timer) { 32 | clearInterval(this.#timer) 33 | this.#interval = newInterval 34 | this.#timer = setInterval(() => this.checkIP(), this.#interval) 35 | } 36 | } 37 | 38 | #notifyIPChange(data) { 39 | MainWindow.window.webContents.send('ip-updated', data) 40 | } 41 | 42 | #handleNewIPRecord(data) { 43 | if (!this.#currentRecord) { 44 | // 首次记录 45 | this.#currentRecord = data 46 | // addRecord(this.#currentRecord) 47 | this.#notifyIPChange(data) 48 | // return 49 | } 50 | 51 | // IP 变化时更新记录 52 | if (this.#currentRecord.ip !== data.ip) { 53 | // addRecord(this.#currentRecord) 54 | this.#currentRecord = data 55 | // addRecord(this.#currentRecord) 56 | this.#notifyIPChange(data) 57 | } 58 | 59 | addRecord(this.#currentRecord) 60 | } 61 | 62 | #initializeTimer() { 63 | const interval = getInterval() 64 | this.#interval = interval.data 65 | this.#timer = setInterval(() => this.checkIP(), this.#interval) 66 | } 67 | 68 | async start() { 69 | if (this.#isRunning) { 70 | return response.error('记录器已在运行中') 71 | } 72 | 73 | try { 74 | const result = await this.checkIP() 75 | if (result.code === -1) { 76 | return response.error(result.message) 77 | } 78 | 79 | this.#isRunning = true 80 | this.#initializeTimer() 81 | return response.success() 82 | } catch (error) { 83 | console.error(error) 84 | return response.error(error.message || '启动过程发生错误') 85 | } 86 | } 87 | 88 | pause() { 89 | if (!this.#isRunning) { 90 | return response.error('未处于记录状态') 91 | } 92 | 93 | if (this.#timer) { 94 | clearInterval(this.#timer) 95 | this.#timer = null 96 | } 97 | this.#isRunning = false 98 | 99 | if (this.#currentRecord?.ip) { 100 | addRecord(this.#currentRecord) 101 | this.#currentRecord = null 102 | } 103 | 104 | return response.success() 105 | } 106 | 107 | async resume() { 108 | if (this.#isRunning) return response.success() 109 | return this.start() 110 | } 111 | 112 | stop() { 113 | this.pause() 114 | this.#currentRecord = null 115 | this.#lastError = null 116 | return response.success() 117 | } 118 | 119 | async checkIP() { 120 | try { 121 | const result = await getInfoWithOpts() 122 | 123 | if (result.code === -1) { 124 | this.#lastError = result.message 125 | return response.error(result.message) 126 | } 127 | 128 | this.#lastError = null 129 | this.#handleNewIPRecord(result.data) 130 | return response.success(result.data) 131 | } catch (error) { 132 | console.error('Failed to fetch IP:', error) 133 | this.#lastError = error.message 134 | return response.error(error.message) 135 | } 136 | } 137 | 138 | getStatus() { 139 | return { 140 | isRunning: this.#isRunning, 141 | currentRecord: this.#currentRecord, 142 | lastError: this.#lastError, 143 | interval: this.#interval 144 | } 145 | } 146 | 147 | destroy() { 148 | this.#unsubscribe?.() 149 | this.stop() 150 | } 151 | } 152 | 153 | // 单例模式 154 | const recorder = new Recorder() 155 | export default recorder 156 | -------------------------------------------------------------------------------- /src/main/services/ipService.mjs: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { createAxiosConfig } from '../utils/proxyWapper.mjs' 3 | import { getConfig } from '../stores/configStore.mjs' 4 | import { getCurrentTime } from '../utils/times.mjs' 5 | import { response } from '../utils/response.mjs' 6 | import { MainWindow } from '../windows/mainWindow.mjs' 7 | 8 | // 获取 IP 归属地的辅助函数 9 | const getLocationByIP = async (ip) => { 10 | try { 11 | const { data } = await axios.post( 12 | 'https://ip.taobao.com/outGetIpInfo', 13 | new URLSearchParams({ ip, accessKey: 'alibaba-inc' }), 14 | { 15 | headers: { 16 | 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8', 17 | 'cache-control': 'no-cache', 18 | origin: 'https://ip.taobao.com', 19 | referer: 'https://ip.taobao.com/ipSearch', 20 | 'user-agent': 21 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36' 22 | } 23 | } 24 | ) 25 | 26 | if (data.data?.country && data.data?.city) { 27 | return `${data.data.country}-${data.data.city}` 28 | } 29 | return null 30 | } catch (error) { 31 | return null 32 | } 33 | } 34 | 35 | // API 配置 36 | const API_CONFIGS = { 37 | 0: { 38 | name: 'ipipnet', 39 | url: 'https://myip.ipip.net/json', 40 | parse: (data) => { 41 | if (!data?.data?.location || !Array.isArray(data.data.location)) { 42 | return response.error('数据格式异常') 43 | } 44 | 45 | return response.success({ 46 | ip: data.data.ip || '未获取到IP', 47 | location: data.data.location.filter(Boolean).join('-') 48 | }) 49 | } 50 | }, 51 | 52 | 1: { 53 | name: 'ipify', 54 | url: 'https://api.ipify.org/?format=json', 55 | parse: async (data) => { 56 | if (!data?.ip) { 57 | return response.error('数据格式异常') 58 | } 59 | 60 | const ip = data.ip 61 | let location = null 62 | let retryCount = 3 63 | 64 | while (retryCount > 0) { 65 | location = await getLocationByIP(ip) 66 | if (location) { 67 | return response.success({ 68 | ip, 69 | location 70 | }) 71 | } 72 | retryCount-- 73 | if (retryCount > 0) { 74 | await new Promise((resolve) => setTimeout(resolve, 1000)) 75 | } 76 | } 77 | 78 | return response.error('获取IP归属地失败') 79 | } 80 | }, 81 | 82 | 2: { 83 | name: 'baidu', 84 | url: 'https://qifu-api.baidubce.com/ip/local/geo/v1/district', 85 | parse: (data) => { 86 | if (!data?.code || data.code !== 'Success') { 87 | return response.error('数据格式异常') 88 | } 89 | 90 | return response.success({ 91 | ip: data.ip || '未获取到IP', 92 | location: [data.data.country, data.data.prov, data.data.city, data.data.isp] 93 | .filter(Boolean) 94 | .join('-') 95 | }) 96 | } 97 | }, 98 | 99 | 3: { 100 | name: 'ip-api', 101 | url: 'http://demo.ip-api.com/json/?lang=zh-CN', 102 | parse: (data) => { 103 | if (!data?.status || data.status !== 'success') { 104 | return response.error('数据格式异常') 105 | } 106 | 107 | return response.success({ 108 | ip: data.query || '未获取到IP', 109 | location: `${data.country}-${data.regionName}-${data.city}` 110 | }) 111 | } 112 | } 113 | } 114 | 115 | // 重试逻辑封装 116 | const withRetry = async (fn, retryCount = 3, delay = 800) => { 117 | let lastError 118 | 119 | for (let i = 0; i <= retryCount; i++) { 120 | try { 121 | // console.log(`尝试第 ${i + 1} 次`) 122 | const result = await fn() 123 | // console.log('请求结果:', result) 124 | // console.log('当前时间:', getCurrentTime()) 125 | return result 126 | } catch (error) { 127 | lastError = error 128 | if (i < retryCount) { 129 | await new Promise((resolve) => setTimeout(resolve, delay)) 130 | } 131 | } 132 | } 133 | 134 | throw lastError 135 | } 136 | 137 | // 获取 IP 信息 138 | const getInfoWithOpts = async (options = {}) => { 139 | try { 140 | const { data: defaultConfig } = getConfig() 141 | const config = { ...defaultConfig, ...options } 142 | const { apiSource, proxy } = config 143 | 144 | const apiConfig = API_CONFIGS[apiSource] 145 | if (!apiConfig) { 146 | return response.error('不支持的 API 来源') 147 | } 148 | 149 | const axiosConfig = createAxiosConfig(proxy.type, proxy.data) 150 | 151 | const result = await withRetry(async () => { 152 | const { data } = await axios.get(apiConfig.url, axiosConfig) 153 | return apiConfig.parse(data) 154 | }, config.retryCounts) 155 | 156 | return result 157 | } catch (error) { 158 | const errorMessage = error.response?.data?.message || error.message || '网络请求失败' 159 | return response.error(errorMessage) 160 | } 161 | } 162 | 163 | // 更新当前信息 164 | const updateCurrentInfo = async () => { 165 | const result = await getInfoWithOpts() 166 | if (result.code === 0) { 167 | return MainWindow.window.webContents.send('ip-updated', result.data) 168 | 169 | } 170 | 171 | return MainWindow.window.webContents.send('ip-updated', null) 172 | } 173 | 174 | export { getInfoWithOpts, updateCurrentInfo } 175 | -------------------------------------------------------------------------------- /src/main/stores/configStore.mjs: -------------------------------------------------------------------------------- 1 | // 用于存储配置项的 store 2 | import Store from 'electron-store' 3 | // import { response } from '../utils/response.mjs' 4 | import { response } from '../utils/response.mjs' 5 | 6 | const STORE_CONFIG = { 7 | encryptionKey: 'ip-recorder-secret', 8 | name: 'config', 9 | fileExtension: 'conf', 10 | defaults: { 11 | proxy: { 12 | type: 0, // 0: 无代理 、 1: 系统代理 、 2: 自定义代理 13 | data: { 14 | protocol: 0, // 0: SOCKS5 、 1: HTTP 、 2: HTTPS 15 | host: '', 16 | port: '', 17 | auth: null 18 | // auth: { 19 | // username: '', // 非必须 20 | // password: '' // 非必须 21 | // } 22 | } 23 | }, 24 | apiSource: 0, 25 | retryCounts: 2, 26 | interval: 10000 27 | } 28 | } 29 | 30 | class ConfigStore { 31 | constructor() { 32 | this.store = new Store(STORE_CONFIG) 33 | } 34 | 35 | getConfig = () => response.success(this.store.get()) 36 | 37 | getProxy = () => { 38 | const proxy = this.store.get('proxy', null) 39 | return proxy ? response.success(proxy) : response.error('未设置代理') 40 | } 41 | 42 | getApiSource = () => { 43 | const apiSource = this.store.get('apiSource', null) 44 | return [0, 1, 2, 3].includes(apiSource) 45 | ? response.success(apiSource) 46 | : response.error('未选择api数据源') 47 | } 48 | 49 | getRetryCounts = () => { 50 | return response.success(this.store.get('retryCounts', null)) 51 | } 52 | 53 | getInterval = () => { 54 | return response.success(this.store.get('interval', null)) 55 | } 56 | 57 | getReqOptions = () => { 58 | const retryCounts = this.store.get('retryCounts', null) 59 | const interval = this.store.get('interval', null) 60 | return response.success({ retryCounts, interval }) 61 | } 62 | 63 | /** 64 | * 设置代理信息 65 | * @param {object} proxy API 来源类型 66 | * @returns 标准return 67 | */ 68 | setProxy = (proxy) => { 69 | try { 70 | // 基础验证 71 | if (!proxy || typeof proxy !== 'object') { 72 | return response.error('无效的代理配置') 73 | } 74 | 75 | // 类型验证 76 | const type = Number(proxy.type) 77 | if (![0, 1, 2].includes(type)) { 78 | return response.error('无效的代理类型') 79 | } 80 | 81 | // 如果类型不是 0(不使用代理), 1(系统代理),则验证必要字段 82 | if (type !== 0 && type !== 1) { 83 | if (!proxy.data?.host || !proxy.data?.port) { 84 | return response.error('代理服务器地址和端口不能为空') 85 | } 86 | 87 | // 验证端口范围 88 | const port = Number(proxy.data.port) 89 | if (isNaN(port) || port < 1 || port > 65535) { 90 | return response.error('无效的端口号') 91 | } 92 | 93 | // 验证协议类型(如果有) 94 | if (proxy.data.protocol !== undefined) { 95 | const protocol = Number(proxy.data.protocol) 96 | if (![0, 1, 2].includes(protocol)) { 97 | return response.error('无效的协议类型') 98 | } 99 | } 100 | } 101 | 102 | // 格式化数据 103 | const sanitizedProxy = { 104 | type, 105 | data: 106 | type === 0 || type === 1 107 | ? null 108 | : { 109 | host: String(proxy.data.host).trim(), 110 | port: String(proxy.data.port).trim(), 111 | protocol: Number(proxy.data?.protocol || 0), 112 | auth: proxy.data?.auth 113 | ? { 114 | username: String(proxy.data.auth.username || '').trim(), 115 | password: String(proxy.data.auth.password || '').trim() 116 | } 117 | : undefined 118 | } 119 | } 120 | 121 | // 写入配置 122 | this.store.set('proxy', sanitizedProxy) 123 | return response.success(sanitizedProxy) 124 | } catch (error) { 125 | return response.error(error.message || '保存代理配置失败') 126 | } 127 | } 128 | 129 | /** 130 | * 设置 API 来源 131 | * @param {number} apiSource API 来源类型 132 | * @returns {Promise<{code: number, data?: any, message?: string}>} 133 | */ 134 | setApiSource = (apiSource) => { 135 | try { 136 | const source = Number(apiSource) 137 | if (![0, 1, 2, 3].includes(source)) { 138 | return response.error('无效的 API 来源类型') 139 | } 140 | 141 | this.store.set('apiSource', source) 142 | return response.success(source) 143 | } catch (error) { 144 | return response.error(error.message || '保存 API 来源配置失败') 145 | } 146 | } 147 | 148 | /** 149 | * 设置重试次数 150 | * @param {number} retryCounts 重试次数 151 | * @returns {Promise<{code: number, data?: any, message?: string}>} 152 | */ 153 | setRetryCounts = (retryCounts) => { 154 | try { 155 | const counts = Number(retryCounts) 156 | if (isNaN(counts) || counts < 0 || counts > 10) { 157 | return response.error('无效的重试次数或重试次数大于10次') 158 | } 159 | 160 | this.store.set('retryCounts', counts) 161 | return response.success(counts) 162 | } catch (error) { 163 | return response.error(error.message || '保存重试次数失败') 164 | } 165 | } 166 | 167 | /** 168 | * 设置记录过程中请求间隔时间 169 | * @param {number} interval 间隔时间 毫秒 170 | * @returns {Promise<{code: number, data?: any, message?: string}>} 171 | */ 172 | setReqInterval = (interval) => { 173 | try { 174 | const counts = Number(interval) 175 | if (isNaN(counts)) return response.error('无效的间隔时间') 176 | if (counts < 500) return response.error('间隔时间不得小于 500 毫秒') 177 | 178 | this.store.set('interval', counts) 179 | return response.success(counts) 180 | } catch (error) { 181 | return response.error(error.message || '保存间隔时间失败') 182 | } 183 | } 184 | 185 | /** 186 | * 187 | * @param {*} callback 当 interval 发生变化时调用的回调函数 188 | * @returns 返回的是取消监听的函数 189 | */ 190 | subscribeIntervalChange = (callback) => { 191 | const unsubscribe = this.store.onDidChange('interval', (newValue, oldValue) => { 192 | callback({ interval: newValue }, { interval: oldValue }) 193 | }) 194 | return unsubscribe 195 | } 196 | } 197 | 198 | const configStore = new ConfigStore() 199 | 200 | export const { 201 | getConfig, 202 | getProxy, 203 | getApiSource, 204 | getRetryCounts, 205 | getInterval, 206 | getReqOptions, 207 | setProxy, 208 | setApiSource, 209 | setRetryCounts, 210 | setReqInterval, 211 | subscribeIntervalChange 212 | } = configStore 213 | -------------------------------------------------------------------------------- /src/main/stores/recorderStore.mjs: -------------------------------------------------------------------------------- 1 | // src/main/store/config.js 2 | import { response } from '../utils/response.mjs' 3 | import { getCurrentTime } from '../utils/times.mjs' 4 | import Store from 'electron-store' 5 | 6 | const store = new Store({ 7 | encryptionKey: 'ip-recorder-secret', 8 | name: 'records', 9 | defaults: { 10 | records: [] // { startTime, endTime, ip, location } 11 | } 12 | }) 13 | 14 | const STORE_CONFIG = { 15 | encryptionKey: 'ip-recorder-secret', 16 | name: 'records', 17 | defaults: { 18 | records: [] 19 | } 20 | } 21 | 22 | const RECORDS_KEY = 'records' 23 | 24 | class RecordStore { 25 | constructor() { 26 | this.store = new Store(STORE_CONFIG) 27 | } 28 | 29 | // 是否存在历史记录 30 | hasHistory = () => { 31 | const records = this.store.get(RECORDS_KEY, []) 32 | 33 | // !! 可以将后面跟的值转为 Boolean 34 | return response.success(!!records.length) 35 | } 36 | /** 37 | * 功能描述: 向 store 中添加IP记录 38 | * @param { ip, location } 组成的对象 39 | */ 40 | addRecord = ({ ip, location = '' }) => { 41 | try { 42 | if (!ip) { 43 | return response.error('IP 地址不能为空') 44 | } 45 | 46 | const records = this.store.get(RECORDS_KEY, []) 47 | const currentTime = getCurrentTime() 48 | const lastRecord = records[records.length - 1] 49 | 50 | if (lastRecord?.ip === ip) { 51 | lastRecord.endTime = currentTime 52 | } else { 53 | records.push({ 54 | ip, 55 | location, 56 | startTime: currentTime, 57 | endTime: currentTime 58 | }) 59 | } 60 | 61 | this.store.set(RECORDS_KEY, records) 62 | return response.success() 63 | } catch (error) { 64 | console.error('添加记录失败:', error) 65 | return response.error('添加记录失败') 66 | } 67 | } 68 | 69 | // 重置记录 70 | resetRecord = () => { 71 | try { 72 | this.store.set(RECORDS_KEY, []) 73 | return response.success() 74 | } catch (error) { 75 | console.error('重置记录失败:', error) 76 | return response.error('重置记录失败') 77 | } 78 | } 79 | 80 | // 获取所有记录 81 | getRecord = () => { 82 | try { 83 | return response.success(this.store.get(RECORDS_KEY, [])) 84 | } catch (error) { 85 | console.error('获取记录失败:', error) 86 | return response.error('获取记录失败') 87 | } 88 | } 89 | } 90 | 91 | const recordStore = new RecordStore() 92 | 93 | export const { hasHistory, getRecord, addRecord, resetRecord, cleanOldRecords } = recordStore 94 | -------------------------------------------------------------------------------- /src/main/system/ipcSetup.mjs: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron' 2 | import { settingsHandlers } from '../ipc/handlers/settingsHandlers.mjs' 3 | import { proxyHandlers } from '../ipc/handlers/proxyHandlers.mjs' 4 | import { apiSourceHandlers } from '../ipc/handlers/apiSourceHandlers.mjs' 5 | import { requestOptionsHandlers } from '../ipc/handlers/requestOptionsHandlers.mjs' 6 | import { recordHandlers } from '../ipc/handlers/recordHandlers.mjs' 7 | 8 | export const setupIPC = () => { 9 | const handlers = { 10 | 'open-settings-window': settingsHandlers.openSetting, 11 | 'close-settings-window': settingsHandlers.closeSetWin, 12 | 'min-main-window': settingsHandlers.minMainWindow, 13 | 'close': settingsHandlers.closeMainWin, 14 | 15 | 'has-history': recordHandlers.hasHistory, 16 | 'reset-record': recordHandlers.resetRecord, 17 | 'export-record': recordHandlers.exportRecord, 18 | 'get-ip-info': recordHandlers.updateCurrentInfo, 19 | 20 | 'get-proxy': proxyHandlers.getProxy, 21 | 'test-proxy': proxyHandlers.testProxy, 22 | 'save-proxy': proxyHandlers.saveProxy, 23 | 24 | 'get-api-source': apiSourceHandlers.getApiSource, 25 | 'test-api-source': apiSourceHandlers.testApiSource, 26 | 'save-api-source': apiSourceHandlers.saveApiSource, 27 | 28 | 'get-request-options': requestOptionsHandlers.getReqOptions, 29 | 'save-request-options': requestOptionsHandlers.saveReqOptions 30 | } 31 | 32 | Object.entries(handlers).forEach(([channel, handler]) => { 33 | ipcMain.handle(channel, handler) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /src/main/system/tray.mjs: -------------------------------------------------------------------------------- 1 | import { app, Tray, Menu, nativeImage } from 'electron' 2 | import { getTrayIconPath } from '../utils/iconUtils.mjs' 3 | import { MainWindow } from '../windows/mainWindow.mjs' 4 | 5 | export class TrayManager { 6 | static tray = null 7 | 8 | static createTray() { 9 | const trayIconPath = getTrayIconPath() 10 | const trayIcon = this.prepareTrayIcon(trayIconPath) 11 | 12 | try { 13 | this.tray = new Tray(trayIcon) 14 | this.setupTray() 15 | } catch (error) { 16 | console.error('Failed to create tray:', error) 17 | throw error 18 | } 19 | } 20 | 21 | static prepareTrayIcon(trayIconPath) { 22 | const icon = nativeImage.createFromPath(trayIconPath) 23 | if (process.platform === 'darwin') { 24 | app.dock.setIcon(icon) 25 | } 26 | return icon.resize({ width: 20, height: 20, quality: 'best' }) 27 | } 28 | 29 | static setupTray() { 30 | this.tray.setToolTip('IP Recorder') 31 | this.tray.setContextMenu(this.createContextMenu()) 32 | // this.tray.on('click', () => MainWindow.show()) 33 | } 34 | 35 | static createContextMenu() { 36 | return Menu.buildFromTemplate([ 37 | { 38 | label: '显示主窗口', 39 | click: () => MainWindow.show() 40 | }, 41 | { type: 'separator' }, 42 | { 43 | label: '退出', 44 | click: () => app.quit() 45 | } 46 | ]) 47 | } 48 | 49 | static destroy() { 50 | if (this.tray) { 51 | this.tray.destroy() 52 | this.tray = null 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/utils/debugConsole.mjs: -------------------------------------------------------------------------------- 1 | // 用于 debug 的输出 2 | 3 | export function print(data) { 4 | console.log('------------------------------------------') 5 | console.log(data) 6 | console.log('------------------------------------------') 7 | } 8 | -------------------------------------------------------------------------------- /src/main/utils/exportToCsv.mjs: -------------------------------------------------------------------------------- 1 | import { dialog, app } from 'electron' 2 | import fs from 'fs' 3 | import path from 'path' 4 | import { getRecord } from '../stores/recorderStore.mjs' 5 | import { response } from './response.mjs' 6 | 7 | async function exportToCSV() { 8 | const { code, data, message } = getRecord() 9 | 10 | if (code === -1) return response.error(message) 11 | if (data.length === 0) return response.error('无IP信息记录') 12 | 13 | const records = data 14 | 15 | // 处理CSV字段中的特殊字符 16 | const escapeCSV = (field) => { 17 | field = String(field) 18 | if (field.includes(',') || field.includes('"') || field.includes('\n')) { 19 | field = field.replace(/"/g, '""') 20 | field = `"${field}"` 21 | } 22 | return field 23 | } 24 | 25 | const headers = ['开始时间', '结束时间', 'IP地址', '归属地'] 26 | 27 | const csvContent = [ 28 | headers.map(escapeCSV).join(','), 29 | ...records.map((record) => 30 | [ 31 | escapeCSV(record.startTime), 32 | escapeCSV(record.endTime), 33 | escapeCSV(record.ip), 34 | escapeCSV(record.location) 35 | ].join(',') 36 | ) 37 | ].join('\n') 38 | 39 | // 添加 BOM,以便 Excel 正确识别中文 40 | const csvData = '\ufeff' + csvContent 41 | 42 | try { 43 | // 打开保存文件对话框 44 | const { filePath } = await dialog.showSaveDialog({ 45 | defaultPath: path.join( 46 | app.getPath('downloads'), 47 | `ip_records_${new Date().toISOString().slice(0, 10)}.csv` 48 | ), 49 | filters: [{ name: 'CSV Files', extensions: ['csv'] }] 50 | }) 51 | 52 | if (filePath) { 53 | // 写入文件 54 | fs.writeFileSync(filePath, csvData, 'utf8') 55 | return response.success(filePath) 56 | } 57 | } catch (error) { 58 | console.error('Export failed:', error) 59 | return response.error(`导出失败: ${error}`) 60 | } 61 | } 62 | 63 | export default exportToCSV 64 | -------------------------------------------------------------------------------- /src/main/utils/iconUtils.mjs: -------------------------------------------------------------------------------- 1 | import winTrayIcon from '../../../build/tray-icons/win/tray-icon.ico?asset' 2 | import macTrayIcon from '../../../build/tray-icons/mac/tray-icon.png?asset' 3 | import linuxTrayIcon from '../../../build/tray-icons/linux/tray-icon.png?asset' 4 | 5 | export const getTrayIconPath = () => { 6 | const icons = { 7 | darwin: macTrayIcon, 8 | win32: winTrayIcon, 9 | linux: linuxTrayIcon 10 | } 11 | return icons[process.platform] || linuxTrayIcon 12 | } 13 | -------------------------------------------------------------------------------- /src/main/utils/paths.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'url' 2 | import { dirname } from 'path' 3 | 4 | export function getDirName(importMetaUrl) { 5 | // 将 import.meta.url 转换为文件系统路径 6 | const __filename = fileURLToPath(importMetaUrl) 7 | // 获取文件所在的目录路径 8 | return dirname(__filename) 9 | } 10 | -------------------------------------------------------------------------------- /src/main/utils/proxyWapper.mjs: -------------------------------------------------------------------------------- 1 | import { HttpProxyAgent } from 'http-proxy-agent' 2 | import { HttpsProxyAgent } from 'https-proxy-agent' 3 | import { SocksProxyAgent } from 'socks-proxy-agent' 4 | 5 | // 封装代理配置函数 6 | export function createAxiosConfig(proxyType, customProxyConfig = null) { 7 | const baseConfig = { 8 | timeout: 5000, 9 | maxRedirects: 5 10 | } 11 | 12 | switch (proxyType) { 13 | case 0: // 无代理 14 | return { 15 | ...baseConfig, 16 | proxy: false, 17 | httpAgent: null, 18 | httpsAgent: null 19 | } 20 | 21 | case 1: // 系统代理 22 | return { 23 | ...baseConfig 24 | } 25 | 26 | case 2: // 自定义代理 27 | if (!customProxyConfig) { 28 | throw new Error('Custom proxy config is required') 29 | } 30 | 31 | const { protocol, host, port, auth } = customProxyConfig 32 | let agent 33 | 34 | // 构建代理URL 35 | const getProxyUrl = () => { 36 | const proto = protocol === 0 ? 'socks5' : protocol === 1 ? 'http' : 'https' 37 | 38 | if (auth && auth.username && auth.password) { 39 | return `${proto}://${auth.username}:${auth.password}@${host}:${port}` 40 | } 41 | return `${proto}://${host}:${port}` 42 | } 43 | 44 | // 创建对应的agent 45 | if (protocol === 0) { 46 | agent = new SocksProxyAgent(getProxyUrl()) 47 | } else if (protocol === 1) { 48 | agent = new HttpProxyAgent(getProxyUrl()) 49 | } else { 50 | agent = new HttpsProxyAgent(getProxyUrl()) 51 | } 52 | 53 | return { 54 | ...baseConfig, 55 | httpAgent: agent, 56 | httpsAgent: agent, 57 | proxy: undefined // 关键:使用agent时不设置proxy 58 | } 59 | 60 | default: 61 | throw new Error('Invalid proxy type') 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/utils/response.mjs: -------------------------------------------------------------------------------- 1 | // 标准 return 2 | 3 | export const response = { 4 | success(data = null, message = 'success') { 5 | return { 6 | code: 0, 7 | data, 8 | message 9 | } 10 | }, 11 | error(error) { 12 | return { 13 | code: -1, 14 | data: null, 15 | message: error instanceof Error ? error.message : String(error) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/utils/times.mjs: -------------------------------------------------------------------------------- 1 | // 获取当前时间 2 | export const getCurrentTime = () => { 3 | return new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }) 4 | } 5 | -------------------------------------------------------------------------------- /src/main/windows/commonWindow.mjs: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from 'electron' 2 | import { path } from 'path' 3 | 4 | export class CommonWindow { 5 | constructor() { 6 | this.window = null 7 | this.defaultOptions = { 8 | webPreferences: { 9 | nodeIntegration: false, 10 | contextIsolation: true, 11 | sandbox: true 12 | } 13 | } 14 | } 15 | 16 | /** 17 | * 创建窗口 18 | * @param {Object} options - 窗口配置选项 19 | * @returns {BrowserWindow} 窗口实例 20 | */ 21 | create(options = {}) { 22 | if (this.isCreated()) { 23 | return this.window 24 | } 25 | 26 | try { 27 | const mergedOptions = this.mergeOptions(options) 28 | this.window = new BrowserWindow(mergedOptions) 29 | this.setupWindowEvents() 30 | return this.window 31 | } catch (error) { 32 | console.error('Failed to create window:', error) 33 | throw new Error('Window creation failed') 34 | } 35 | } 36 | 37 | /** 38 | * 合并默认配置和用户配置 39 | * @param {Object} options 40 | */ 41 | mergeOptions(options) { 42 | return { 43 | ...this.defaultOptions, 44 | ...options, 45 | webPreferences: { 46 | ...this.defaultOptions.webPreferences, 47 | ...options.webPreferences 48 | } 49 | } 50 | } 51 | 52 | /** 53 | * 设置窗口事件监听 54 | */ 55 | setupWindowEvents() { 56 | if (!this.window) return 57 | 58 | this.window.on('closed', () => { 59 | this.window = null 60 | }) 61 | 62 | this.window.on('unresponsive', () => { 63 | console.warn('Window became unresponsive') 64 | }) 65 | 66 | this.window.webContents.on('crashed', () => { 67 | console.error('Window crashed') 68 | }) 69 | } 70 | 71 | /** 72 | * 向窗口发送消息 73 | * @param {string} channel - 消息通道 74 | * @param {...any} args - 消息参数 75 | */ 76 | send(channel, ...args) { 77 | try { 78 | if (!this.isCreated()) return false 79 | this.window.webContents.send(channel, ...args) 80 | return true 81 | } catch (error) { 82 | console.error('Failed to send message:', error) 83 | return false 84 | } 85 | } 86 | 87 | /** 88 | * 获取窗口实例 89 | */ 90 | getWindow() { 91 | return this.window 92 | } 93 | 94 | /** 95 | * 检查窗口是否已创建且未销毁 96 | */ 97 | isCreated() { 98 | return !!this.window && !this.window.isDestroyed() 99 | } 100 | 101 | isVisible() { 102 | return !!this.window && this.window.isVisible() 103 | } 104 | 105 | /** 106 | * 执行窗口操作 107 | * @param {Function} action - 要执行的操作 108 | */ 109 | async executeWindowAction(action) { 110 | if (!this.isCreated()) return false 111 | try { 112 | await action() 113 | return true 114 | } catch (error) { 115 | console.error(`Window action failed:`, error) 116 | return false 117 | } 118 | } 119 | 120 | async center() { 121 | return this.executeWindowAction(() => this.window.center()) 122 | } 123 | 124 | async focus() { 125 | return this.executeWindowAction(() => this.window.focus()) 126 | } 127 | 128 | async minimize() { 129 | return this.executeWindowAction(() => this.window.minimize()) 130 | } 131 | 132 | async hide() { 133 | return this.executeWindowAction(() => this.window.hide()) 134 | } 135 | 136 | async show() { 137 | if (this.isCreated()) { 138 | return this.executeWindowAction(() => this.window.show()) 139 | } 140 | try { 141 | this.create() 142 | return true 143 | } catch { 144 | return false 145 | } 146 | } 147 | 148 | /** 149 | * 关闭窗口 150 | */ 151 | async close() { 152 | if (!this.isCreated()) return true 153 | 154 | try { 155 | await this.executeWindowAction(() => this.window.close()) 156 | this.window = null 157 | return true 158 | } catch (error) { 159 | console.error('Failed to close window:', error) 160 | return false 161 | } 162 | } 163 | 164 | /** 165 | * 销毁窗口实例 166 | */ 167 | destroy() { 168 | if (!this.isCreated()) return 169 | 170 | try { 171 | this.window.destroy() 172 | this.window = null 173 | } catch (error) { 174 | console.error('Failed to destroy window:', error) 175 | } 176 | } 177 | } 178 | 179 | export default CommonWindow 180 | -------------------------------------------------------------------------------- /src/main/windows/mainWindow.mjs: -------------------------------------------------------------------------------- 1 | import { shell, app } from 'electron' 2 | import { is } from '@electron-toolkit/utils' 3 | import { join } from 'path' 4 | import { CommonWindow } from './commonWindow.mjs' 5 | import { getDirName } from '../utils/paths.mjs' 6 | import icon from '../../../build/icon.png?asset' 7 | import Main from 'electron/main' 8 | 9 | const currentDir = getDirName(import.meta.url) 10 | 11 | class MainWindow extends CommonWindow { 12 | static #instance = null 13 | 14 | /** 15 | * 默认窗口配置 16 | */ 17 | #defaultConfig = { 18 | width: 500, 19 | height: 118, 20 | show: false, 21 | frame: false, 22 | resizable: false, 23 | autoHideMenuBar: true, 24 | transparent: true, 25 | backgroundColor: '#00000000', 26 | ...(process.platform === 'linux' ? { icon } : {}), 27 | webPreferences: { 28 | preload: join(currentDir, '../preload/main/index.js') 29 | // sandbox: false 30 | } 31 | } 32 | 33 | constructor() { 34 | if (MainWindow.#instance) { 35 | return MainWindow.#instance 36 | } 37 | super() 38 | MainWindow.#instance = this 39 | this.#setupAppEvents() 40 | } 41 | 42 | /** 43 | * 设置应用级事件监听 44 | */ 45 | #setupAppEvents() { 46 | app.on('before-quit', () => { 47 | this.destroy() 48 | }) 49 | 50 | app.on('activate', () => { 51 | if (this.isCreated() && !this.isVisible()) { 52 | this.show() 53 | } else if (!this.isCreated()) { 54 | this.create() 55 | } 56 | }) 57 | } 58 | 59 | /** 60 | * 设置窗口事件监听 61 | */ 62 | #setupWindowEvents(window) { 63 | window.on('ready-to-show', () => { 64 | window.show() 65 | }) 66 | 67 | window.webContents.setWindowOpenHandler((details) => { 68 | shell.openExternal(details.url) 69 | return { action: 'deny' } 70 | }) 71 | } 72 | 73 | /** 74 | * 加载窗口内容 75 | */ 76 | async #loadContent(window) { 77 | try { 78 | if (is.dev && process.env['ELECTRON_RENDERER_URL']) { 79 | const url = `${process.env['ELECTRON_RENDERER_URL']}/src/main/main.html` 80 | // console.log('Loading dev URL:', url) 81 | await window.loadURL(url) 82 | } else { 83 | const filePath = join(currentDir, '../renderer/src/main/main.html') 84 | await window.loadFile(filePath) 85 | } 86 | } catch (error) { 87 | console.error('Failed to load window content:', error) 88 | throw error 89 | } 90 | } 91 | 92 | /** 93 | * 创建主窗口 94 | */ 95 | create() { 96 | try { 97 | const window = super.create(this.#defaultConfig) 98 | window.shadow = true 99 | this.#setupWindowEvents(window) 100 | this.#loadContent(window) 101 | return window 102 | } catch (error) { 103 | console.error('Failed to create main window:', error) 104 | throw error 105 | } 106 | } 107 | 108 | /** 109 | * 重写销毁方法,清理单例 110 | */ 111 | destroy() { 112 | super.destroy() 113 | MainWindow.#instance = null 114 | } 115 | 116 | /** 117 | * 获取单例 118 | */ 119 | static getInstance() { 120 | if (!MainWindow.#instance) { 121 | MainWindow.#instance = new MainWindow() 122 | } 123 | return MainWindow.#instance 124 | } 125 | 126 | 127 | } 128 | 129 | // 创建单例并导出,保持与原代码相同的导出方式 130 | const mainWindow = new MainWindow() 131 | export { mainWindow as MainWindow } 132 | -------------------------------------------------------------------------------- /src/main/windows/settingsWindow.mjs: -------------------------------------------------------------------------------- 1 | import { is } from '@electron-toolkit/utils' 2 | import { shell } from 'electron' 3 | import { join } from 'path' 4 | import { CommonWindow } from './commonWindow.mjs' 5 | import { getDirName } from '../utils/paths.mjs' 6 | import icon from '../../../build/icon.png?asset' 7 | 8 | const currentDir = getDirName(import.meta.url) 9 | 10 | class SettingsWindowClass extends CommonWindow { 11 | static #instance = null 12 | 13 | /** 14 | * 默认窗口配置 15 | */ 16 | #defaultConfig = { 17 | width: 600, 18 | height: 222, 19 | show: false, 20 | frame: false, 21 | resizable: false, 22 | autoHideMenuBar: true, 23 | transparent: true, 24 | backgroundColor: '#00000000', 25 | ...(process.platform === 'linux' ? { icon } : {}), 26 | webPreferences: { 27 | preload: join(currentDir, '../preload/settings/index.js'), 28 | // contextIsolation: true, 29 | // sandbox: true 30 | } 31 | } 32 | 33 | constructor() { 34 | if (SettingsWindowClass.#instance) { 35 | return SettingsWindowClass.#instance 36 | } 37 | super() 38 | SettingsWindowClass.#instance = this 39 | } 40 | 41 | /** 42 | * 设置窗口事件监听 43 | */ 44 | #setupWindowEvents(window) { 45 | window.on('ready-to-show', () => { 46 | window.show() 47 | }) 48 | 49 | window.webContents.setWindowOpenHandler((details) => { 50 | shell.openExternal(details.url) 51 | return { action: 'deny' } 52 | }) 53 | 54 | // 可以添加关闭时的处理 55 | // window.on('close', () => { 56 | // // 保存设置等 57 | // this.saveSettings() 58 | // }) 59 | } 60 | 61 | /** 62 | * 加载窗口内容 63 | */ 64 | async #loadContent(window) { 65 | try { 66 | if (is.dev && process.env['ELECTRON_RENDERER_URL']) { 67 | const url = `${process.env['ELECTRON_RENDERER_URL']}/src/settings/settings.html` 68 | await window.loadURL(url) 69 | } else { 70 | const filePath = join(currentDir, '../renderer/src/settings/settings.html') 71 | await window.loadFile(filePath) 72 | } 73 | } catch (error) { 74 | console.error('Failed to load settings content:', error) 75 | throw error 76 | } 77 | } 78 | 79 | /** 80 | * 创建设置窗口 81 | */ 82 | create() { 83 | try { 84 | const window = super.create(this.#defaultConfig) 85 | window.shadow = true 86 | this.#setupWindowEvents(window) 87 | this.#loadContent(window) 88 | return window 89 | } catch (error) { 90 | console.error('Failed to create settings window:', error) 91 | throw error 92 | } 93 | } 94 | 95 | /** 96 | * 创建或聚焦窗口 97 | */ 98 | async createOrFocus() { 99 | try { 100 | if (this.isCreated()) { 101 | const window = this.getWindow() 102 | 103 | if (window.isMinimized()) { 104 | window.restore() 105 | } 106 | 107 | await this.center() 108 | window.focus() 109 | // return window 110 | } 111 | 112 | return this.create() 113 | } catch (error) { 114 | console.error('Failed to create or focus settings window:', error) 115 | throw error 116 | } 117 | } 118 | 119 | /** 120 | * 重写销毁方法,清理单例 121 | */ 122 | destroy() { 123 | super.destroy() 124 | SettingsWindowClass.#instance = null 125 | } 126 | } 127 | 128 | // 创建单例并导出 129 | const settingsWindow = new SettingsWindowClass() 130 | export { settingsWindow as SettingsWindow } 131 | -------------------------------------------------------------------------------- /src/preload/main/api/ipHandler.js: -------------------------------------------------------------------------------- 1 | // 用于处理IP相关的 ipc api 2 | export const ipHandler = { 3 | // 通过自定义事件的方式让渲染进程实时更新IP信息 4 | setupIPUpdate(ipcRenderer) { 5 | ipcRenderer.on('ip-updated', (_event, data) => { 6 | // 触发自定义事件,将数据传递给渲染进程 7 | window.dispatchEvent(new CustomEvent('ip-update', { detail: data })) 8 | }) 9 | }, 10 | 11 | getApi(ipcRenderer) { 12 | return { 13 | updateIPInfo: () => ipcRenderer.invoke('get-ip-info') 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/preload/main/api/recordControl.js: -------------------------------------------------------------------------------- 1 | // 用于处理记录相关的 ipc api 2 | export const recordControl = { 3 | getApi(ipcRenderer) { 4 | return { 5 | start: () => ipcRenderer.invoke('start-record'), 6 | pause: () => ipcRenderer.invoke('pause-record'), 7 | resume: () => ipcRenderer.invoke('resume-record'), 8 | stop: () => ipcRenderer.invoke('stop-record'), 9 | hasHistory: () => ipcRenderer.invoke('has-history'), 10 | export: () => ipcRenderer.invoke('export-record'), 11 | resetRecord: () => ipcRenderer.invoke('reset-record') 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/preload/main/api/windowControl.js: -------------------------------------------------------------------------------- 1 | // 用于处理窗口相关的 ipc api 2 | export const windowControl = { 3 | getApi(ipcRenderer) { 4 | return { 5 | openSettings: () => ipcRenderer.invoke('open-settings-window'), 6 | close: () => ipcRenderer.invoke('quit'), 7 | minimize: () => ipcRenderer.invoke('min-main-window') 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/preload/main/index-with-tear.js: -------------------------------------------------------------------------------- 1 | const { contextBridge, ipcRenderer } = require('electron') 2 | const { ipHandler } = require('./api/ipHandler') 3 | const { windowControl } = require('./api/windowControl') 4 | const { recordControl } = require('./api/recordControl') 5 | 6 | /** 7 | * 创建完整的 API 对象 8 | */ 9 | function createAPI() { 10 | return { 11 | ...ipHandler.getApi(ipcRenderer), 12 | ...windowControl.getApi(ipcRenderer), 13 | ...recordControl.getApi(ipcRenderer) 14 | } 15 | } 16 | 17 | /** 18 | * 设置所有事件监听器 19 | */ 20 | function setupListeners() { 21 | ipHandler.setupIPUpdate(ipcRenderer) 22 | } 23 | 24 | /** 25 | * 初始化函数 26 | */ 27 | function initialize() { 28 | try { 29 | setupListeners() 30 | const api = createAPI() 31 | 32 | if (process.contextIsolated) { 33 | try { 34 | contextBridge.exposeInMainWorld('api', api) 35 | } catch (error) { 36 | console.error(error) 37 | } 38 | } else { 39 | // window.electron = electronAPI 40 | window.api = api 41 | } 42 | } catch (error) { 43 | console.error('Preload initialization failed:', error) 44 | throw error 45 | } 46 | } 47 | 48 | // 执行初始化 49 | initialize() 50 | -------------------------------------------------------------------------------- /src/preload/main/index.js: -------------------------------------------------------------------------------- 1 | const { contextBridge, ipcRenderer } = require('electron') 2 | // import { electronAPI } from '@electron-toolkit/preload' 3 | 4 | // 通过自定义事件的方式让渲染进程实时更新IP信息 5 | const setupIPUpdate = () => { 6 | // 监听主进程发来的IP更新信息 7 | ipcRenderer.on('ip-updated', (_event, data) => { 8 | // 触发自定义事件,将数据传递给渲染进程 9 | window.dispatchEvent(new CustomEvent('ip-update', { detail: data })) 10 | }) 11 | } 12 | 13 | // Custom APIs for renderer 14 | const api = { 15 | openSettings: () => ipcRenderer.invoke('open-settings-window'), // 打开设置窗口 16 | close: () => ipcRenderer.invoke('close'), 17 | minimize: () => ipcRenderer.invoke('min-main-window'), 18 | updateIPInfo: () => ipcRenderer.invoke('get-ip-info'), 19 | start: () => ipcRenderer.invoke('start-record'), 20 | pause: () => ipcRenderer.invoke('pause-record'), 21 | resume: () => ipcRenderer.invoke('resume-record'), 22 | stop: () => ipcRenderer.invoke('stop-record'), 23 | hasHistory: () => ipcRenderer.invoke('has-history'), 24 | export: () => ipcRenderer.invoke('export-record'), 25 | resetRecord: () => ipcRenderer.invoke('reset-record') 26 | } 27 | 28 | // Use contextBridge APIs to expose Electron APIs to 29 | // renderer only if context isolation is enabled, otherwise 30 | // just add to the DOM global. 31 | if (process.contextIsolated) { 32 | try { 33 | contextBridge.exposeInMainWorld('api', api) 34 | } catch (error) { 35 | console.error(error) 36 | } 37 | } else { 38 | // window.electron = electronAPI 39 | window.api = api 40 | } 41 | 42 | // 初始化监听 43 | setupIPUpdate() 44 | -------------------------------------------------------------------------------- /src/preload/settings/api/apiSourceControl.js: -------------------------------------------------------------------------------- 1 | export const apiSourceControl = { 2 | getApi(ipcRenderer) { 3 | return { 4 | getApiSource: () => ipcRenderer.invoke('get-api-source'), 5 | testApiSource: (data) => ipcRenderer.invoke('test-api-source', data), 6 | saveApiSource: (data) => ipcRenderer.invoke('save-api-source', data) 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/preload/settings/api/proxyControl.js: -------------------------------------------------------------------------------- 1 | export const proxyControl = { 2 | getApi(ipcRenderer) { 3 | return { 4 | getProxy: () => ipcRenderer.invoke('get-proxy'), 5 | testProxy: (data) => ipcRenderer.invoke('test-proxy', data), 6 | saveProxy: (data) => ipcRenderer.invoke('save-proxy', data) 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/preload/settings/api/requestOptionsControl.js: -------------------------------------------------------------------------------- 1 | export const requestOptionsControl = { 2 | getApi(ipcRenderer) { 3 | return { 4 | getReqOptions: () => ipcRenderer.invoke('get-request-options'), 5 | saveReqOptions: (data) => ipcRenderer.invoke('save-request-options', data) 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/preload/settings/api/windowControl.js: -------------------------------------------------------------------------------- 1 | // api/windowControl.js 2 | export const windowControl = { 3 | getApi(ipcRenderer) { 4 | return { 5 | close: () => ipcRenderer.invoke('close-settings-window') 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/preload/settings/index-with-tear.js: -------------------------------------------------------------------------------- 1 | const { contextBridge, ipcRenderer } = require('electron') 2 | const { windowControl } = require('./api/windowControl') 3 | const { proxyControl } = require('./api/proxyControl') 4 | const { apiSourceControl } = require('./api/apiSourceControl') 5 | const { requestOptionsControl } = require('./api/requestOptionsControl') 6 | 7 | 8 | /** 9 | * 创建完整的 API 对象 10 | */ 11 | const createAPI = () => { 12 | return { 13 | ...windowControl.getApi(ipcRenderer), 14 | ...proxyControl.getApi(ipcRenderer), 15 | ...apiSourceControl.getApi(ipcRenderer), 16 | ...requestOptionsControl.getApi(ipcRenderer) 17 | } 18 | } 19 | 20 | const initialize = () => { 21 | try { 22 | console.log('Settings preload initializing...') 23 | const api = createAPI() 24 | 25 | if (process.contextIsolated) { 26 | try { 27 | // contextBridge.exposeInMainWorld('electron', electronAPI) 28 | contextBridge.exposeInMainWorld('api', api) 29 | } catch (error) { 30 | console.error(error) 31 | } 32 | } else { 33 | // window.electron = electronAPI 34 | window.api = api 35 | } 36 | } catch (error) { 37 | console.error('Settings preload initialization failed:', error) 38 | throw error 39 | } 40 | } 41 | 42 | initialize() 43 | -------------------------------------------------------------------------------- /src/preload/settings/index.js: -------------------------------------------------------------------------------- 1 | const { contextBridge, ipcRenderer } = require('electron') 2 | // import { electronAPI } from '@electron-toolkit/preload' 3 | 4 | 5 | // Custom APIs for renderer 6 | const api = { 7 | close: () => ipcRenderer.invoke('close-settings-window'), 8 | getProxy: () => ipcRenderer.invoke('get-proxy'), 9 | testProxy: (data) => ipcRenderer.invoke('test-proxy', data), 10 | saveProxy: (data) => ipcRenderer.invoke('save-proxy', data), 11 | getApiSource: () => ipcRenderer.invoke('get-api-source'), 12 | testApiSource: (data) => ipcRenderer.invoke('test-api-source', data), 13 | saveApiSource: (data) => ipcRenderer.invoke('save-api-source', data), 14 | getReqOptions: () => ipcRenderer.invoke('get-request-options'), 15 | saveReqOptions: (data) => ipcRenderer.invoke('save-request-options', data), 16 | updateIPInfo: () => ipcRenderer.invoke('get-ip-info') 17 | } 18 | 19 | // Use contextBridge APIs to expose Electron APIs to 20 | // renderer only if context isolation is enabled, otherwise 21 | // just add to the DOM global. 22 | if (process.contextIsolated) { 23 | try { 24 | // contextBridge.exposeInMainWorld('electron', electronAPI) 25 | contextBridge.exposeInMainWorld('api', api) 26 | } catch (error) { 27 | console.error(error) 28 | } 29 | } else { 30 | // window.electron = electronAPI 31 | window.api = api 32 | } 33 | -------------------------------------------------------------------------------- /src/renderer/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | // biome-ignore lint: disable 7 | export {} 8 | declare global { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/renderer/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | export {} 6 | 7 | /* prettier-ignore */ 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | ActionBtn: typeof import('./src/components/main/layout/ActionBtn.vue')['default'] 11 | ApiCards: typeof import('./src/components/settings/contents/apiCard/apiCards.vue')['default'] 12 | ApiSource: typeof import('./src/components/settings/contents/ApiSource.vue')['default'] 13 | BtnGroup: typeof import('./src/components/settings/utils/BtnGroup.vue')['default'] 14 | ComfirmBox: typeof import('./src/components/utils/ComfirmBox.vue')['default'] 15 | Content: typeof import('./src/components/settings/layout/Content.vue')['default'] 16 | DragBar: typeof import('./src/components/settings/layout/DragBar.vue')['default'] 17 | Header: typeof import('./src/components/main/layout/Header.vue')['default'] 18 | IPInfo: typeof import('./src/components/main/layout/IPInfo.vue')['default'] 19 | MessageBox: typeof import('./src/components/utils/MessageBox.vue')['default'] 20 | Proxy: typeof import('./src/components/settings/contents/Proxy.vue')['default'] 21 | RequestOpt: typeof import('./src/components/settings/contents/RequestOpt.vue')['default'] 22 | SaveBtn: typeof import('./src/components/settings/utils/SaveBtn.vue')['default'] 23 | TestBtn: typeof import('./src/components/settings/utils/TestBtn.vue')['default'] 24 | Theme: typeof import('./src/components/settings/contents/Theme.vue')['default'] 25 | ToolBar: typeof import('./src/components/main/layout/ToolBar.vue')['default'] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/renderer/src/assets/image/api-select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/api-select.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/api.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/baidu-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/baidu-logo.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/baidu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/baidu.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/close.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/down.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/export.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/ip-api-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/ip-api-logo.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/ip-api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/ip-api.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/ipify-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/ipify-logo.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/ipify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/ipify.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/ipinfo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/ipinfo.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/ipinfo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/renderer/src/assets/image/ipipnet-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/ipipnet-logo.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/ipipnet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/ipipnet.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/line-l.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/line-l.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/line-s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/line-s.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/meitu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/meitu.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/minimize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/minimize.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/pause-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/pause-dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/pause.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/proxy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/proxy.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/radio-selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/radio-selected.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/radio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/radio.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/request.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/select-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/select-button.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/selected-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/selected-button.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/settings.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/start-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/start-dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/start.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/stop-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/stop-dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/stop-enabled-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/stop-enabled-dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/stop-enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/stop-enabled.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/stop.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/theme-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/theme-dark.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/theme-round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/theme-round.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/theme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/theme.png -------------------------------------------------------------------------------- /src/renderer/src/assets/image/up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/image/up.png -------------------------------------------------------------------------------- /src/renderer/src/assets/styles/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin proxy-input { 2 | border-radius: 2px; 3 | border: 1px solid var(--proxy-input-border); 4 | background: var(--proxy-input-bg); 5 | // filter: blur(0.5px); 6 | 7 | color: var(--set-content-text); 8 | 9 | &:hover { 10 | border: 1px solid #18a058; 11 | } 12 | } 13 | 14 | @mixin indent-6px { 15 | display: flex; 16 | align-items: center; 17 | padding-left: 6px; 18 | } 19 | 20 | @mixin my-hover { 21 | &:hover { 22 | // opacity: 0.8; 23 | -webkit-user-drag: none; 24 | &:hover { 25 | transform: scale(1.05); 26 | box-shadow: 0 5px 15px rgba(var(--ctrl-bg), 0.1); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/renderer/src/assets/styles/_variables.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Just-Hack-For-Fun/IP-Recorder/aa1a4a32cb3b5de527232dd7ac56248065aeab6f/src/renderer/src/assets/styles/_variables.scss -------------------------------------------------------------------------------- /src/renderer/src/assets/styles/base.scss: -------------------------------------------------------------------------------- 1 | /* 重置所有元素的默认样式 */ 2 | * { 3 | margin: 0; 4 | padding: 0; 5 | box-sizing: border-box; 6 | } 7 | 8 | /* 重置列表样式 */ 9 | li, ul, ol { 10 | list-style: none; 11 | } 12 | 13 | /* 重置链接样式 */ 14 | a { 15 | text-decoration: none; 16 | color: inherit; 17 | } 18 | 19 | /* 重置按钮样式 */ 20 | button { 21 | border: none; 22 | background: none; 23 | cursor: pointer; 24 | } 25 | 26 | /* 重置输入框样式 */ 27 | input, textarea { 28 | outline: none; 29 | border: none; 30 | } 31 | 32 | /* 设置根字体大小,方便使用 rem */ 33 | html { 34 | font-size: 16px; 35 | } 36 | 37 | /* 平滑滚动 */ 38 | html { 39 | scroll-behavior: smooth; 40 | } 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/renderer/src/assets/styles/index.scss: -------------------------------------------------------------------------------- 1 | @forward '_variables.scss'; 2 | @forward '_mixins.scss'; 3 | @forward 'base.scss'; 4 | @forward 'theme.scss'; 5 | -------------------------------------------------------------------------------- /src/renderer/src/assets/styles/theme.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | // 浅色模式默认值 3 | --bg: #ffffff; 4 | --ip-addr-fill: #f58e6e; 5 | --ip-addr-text: #fffafa; 6 | --ip-location-fill: #ffd452; 7 | --ip-location-text: #fffafa; 8 | --ctrl-bg: rgba(229, 230, 233, 0.3); 9 | --actions-bg: #2c2c2c; 10 | --set-side-text: #2c2c2c; 11 | --set-side-sep: #efeff4; 12 | --set-menu-hover: #f3f3f5; 13 | --set-menu-select: #ebf5ef; 14 | --set-content-text: #2c2c2c; 15 | --set-btn-text: #ffffff; 16 | --proxy-border: #e5e5e5; 17 | --proxy-bg: #ffffff; 18 | --proxy-input-bg: #fdfdfd; 19 | --proxy-select-bg: #fefefe; 20 | --proxy-select-hover: #f3f3f5; 21 | --proxy-input-border: #e5e5e5; 22 | --set-test-btn: #f58e6e; 23 | --set-save-btn: #ffd452; 24 | --request-border: #e5e5e5; 25 | --api-sep-line: #f4f4f4; 26 | --api-card-bg: #ffffff; 27 | --api-single-card-bg: #f5f7fa; 28 | --api-single-card-text: #5E6D82; 29 | --api-single-card-border: #E4E7ED; 30 | --api-card-selected-bg: #F2F6FC; 31 | --api-card-selected-text: #1989FA; 32 | --api-card-selected-border: #D9ECFF; 33 | --request-card-bg: #ffffff; 34 | --theme-card-bg: #ffffff; 35 | --confirm-box-bg: rgba(255, 255, 255, 0.60); 36 | --confirm-box-shadow: rgba(0, 0, 0, 0.25); 37 | --confirm-content-bg: #FFFFFF; 38 | --confirm-content-border: #2C2C2C; 39 | 40 | } 41 | 42 | [data-theme='dark'] { 43 | --bg: #2c2c2c; 44 | --ip-addr-fill: #3a2e40; 45 | --ip-addr-text: #a2b478; 46 | --ip-location-fill: #a0333b; 47 | --ip-location-text: #c6baa9; 48 | --ctrl-bg: rgba(56, 59, 67, 0.3); 49 | --actions-bg: #603d3d; 50 | --set-side-text: #ffffff; 51 | --set-side-sep: #3b3b3b; 52 | --set-menu-hover: #1867bf; 53 | --set-menu-select: #3b3b3b; 54 | --set-content-text: #ffffff; 55 | --set-btn-text: #ffffff; 56 | --proxy-border: #292929; 57 | --proxy-bg: rgba(56, 59, 67, 0.2); 58 | --proxy-input-bg: rgba(56, 59, 67, 0.3); 59 | --proxy-select-bg: rgba(56, 59, 67); 60 | --proxy-select-hover: #2a2a2a; 61 | --proxy-input-border: #514d4d; 62 | --set-test-btn: #603234; 63 | --set-save-btn: #28755d; 64 | --request-border: #292929; 65 | --api-sep-line: rgba(52, 52, 52, 0.9); 66 | --api-card-bg: rgba(56, 59, 67, 0.3); 67 | --api-single-card-bg: #2D2D2D; 68 | --api-single-card-text: #E0E0E0; 69 | --api-single-card-border: #404040; 70 | --api-card-selected-bg: #2B3647; 71 | --api-card-selected-text: #7EB9FF; 72 | --api-card-selected-border: #1C2D46; 73 | --request-card-bg: rgba(56, 59, 67, 0.2); 74 | --theme-card-bg: rgba(56, 59, 67, 0.2); 75 | --confirm-box-bg: rgba(0, 0, 0, 0.60); 76 | --confirm-box-shadow: rgba(255, 255, 255, 0.25); 77 | --confirm-content-bg: #2C2C2C; 78 | --confirm-content-border: #FFFFFF; 79 | } 80 | -------------------------------------------------------------------------------- /src/renderer/src/components/main/layout/ActionBtn.vue: -------------------------------------------------------------------------------- 1 | 108 | 109 | 144 | 145 | 187 | -------------------------------------------------------------------------------- /src/renderer/src/components/main/layout/Header.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 19 | -------------------------------------------------------------------------------- /src/renderer/src/components/main/layout/IPInfo.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 64 | 65 | 162 | -------------------------------------------------------------------------------- /src/renderer/src/components/main/layout/ToolBar.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 69 | 113 | 114 | 229 | -------------------------------------------------------------------------------- /src/renderer/src/components/settings/contents/ApiSource.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 60 | 61 | 107 | -------------------------------------------------------------------------------- /src/renderer/src/components/settings/contents/Proxy.vue: -------------------------------------------------------------------------------- 1 | 178 | 179 | 220 | 221 | 442 | -------------------------------------------------------------------------------- /src/renderer/src/components/settings/contents/RequestOpt.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 56 | 57 | 153 | -------------------------------------------------------------------------------- /src/renderer/src/components/settings/contents/Theme.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 38 | 39 | 178 | -------------------------------------------------------------------------------- /src/renderer/src/components/settings/contents/apiCard/apiCards.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 56 | 57 | 155 | -------------------------------------------------------------------------------- /src/renderer/src/components/settings/layout/Content.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 55 | 56 | 149 | -------------------------------------------------------------------------------- /src/renderer/src/components/settings/layout/DragBar.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | -------------------------------------------------------------------------------- /src/renderer/src/components/settings/utils/BtnGroup.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 24 | 25 | 34 | -------------------------------------------------------------------------------- /src/renderer/src/components/settings/utils/SaveBtn.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 32 | -------------------------------------------------------------------------------- /src/renderer/src/components/settings/utils/TestBtn.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 36 | -------------------------------------------------------------------------------- /src/renderer/src/components/utils/ComfirmBox.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 34 | 35 | 150 | -------------------------------------------------------------------------------- /src/renderer/src/components/utils/MessageBox.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 33 | 34 | 85 | -------------------------------------------------------------------------------- /src/renderer/src/main/App.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 30 | 31 | 64 | -------------------------------------------------------------------------------- /src/renderer/src/main/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | Main Window 22 | 23 | 24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/renderer/src/main/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | // 状态管理 5 | import { createPinia } from 'pinia' 6 | import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' 7 | 8 | const pinia = createPinia() 9 | pinia.use(piniaPluginPersistedstate) 10 | 11 | const app = createApp(App) 12 | app.use(pinia) 13 | app.mount('#app') 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/renderer/src/settings/Settings.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 21 | 22 | 34 | -------------------------------------------------------------------------------- /src/renderer/src/settings/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | Settings 22 | 23 | 24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/renderer/src/settings/settings.js: -------------------------------------------------------------------------------- 1 | // import '@styles/index.scss' 2 | 3 | import { createApp } from 'vue' 4 | import Settings from './Settings.vue' 5 | 6 | // 状态管理 7 | import { createPinia } from 'pinia' 8 | import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' 9 | 10 | const pinia = createPinia() 11 | pinia.use(piniaPluginPersistedstate) 12 | 13 | const app = createApp(Settings) 14 | app.use(pinia) 15 | 16 | app.mount('#app') 17 | -------------------------------------------------------------------------------- /src/renderer/src/stores/theme.js: -------------------------------------------------------------------------------- 1 | // stores/theme.ts 2 | import { defineStore } from 'pinia' 3 | import { ref } from 'vue' 4 | 5 | export const useThemeStore = defineStore('theme', () => { 6 | const isDark = ref(false) 7 | 8 | const handleStorageChange = (event) => { 9 | if (event.key === 'theme-storage') { 10 | const newState = JSON.parse(event.newValue) 11 | isDark.value = newState.isDark 12 | applyTheme() 13 | } 14 | } 15 | 16 | const initThemeSync = () => { 17 | // 初始化时应用主题 18 | applyTheme() 19 | window.addEventListener('storage', handleStorageChange) 20 | } 21 | 22 | const toggleTheme = () => { 23 | isDark.value = !isDark.value 24 | applyTheme() 25 | } 26 | 27 | const applyTheme = () => { 28 | document.documentElement.setAttribute('data-theme', isDark.value ? 'dark' : 'light') 29 | } 30 | 31 | return { 32 | isDark, 33 | toggleTheme, 34 | applyTheme, 35 | initThemeSync 36 | } 37 | }, { 38 | persist: { 39 | key: 'theme-storage', 40 | storage: localStorage, 41 | paths: ['isDark'] 42 | } 43 | }) 44 | -------------------------------------------------------------------------------- /src/renderer/src/utils/useMessage.js: -------------------------------------------------------------------------------- 1 | import { createVNode, render } from 'vue' 2 | // import Message from './Message.vue' 3 | import MessageBox from '@components/utils/MessageBox.vue' 4 | 5 | const messageInstances = [] 6 | 7 | export function useMessage() { 8 | const showMessage = (options) => { 9 | const container = document.createElement('div') 10 | 11 | // 创建消息实例 12 | const vnode = createVNode(MessageBox, { 13 | content: typeof options === 'string' ? options : options.content, 14 | type: options.type || 'info', 15 | duration: options.duration || 3000, 16 | onDestroy: () => { 17 | render(null, container) 18 | document.body.removeChild(container) 19 | const index = messageInstances.indexOf(container) 20 | if (index !== -1) { 21 | messageInstances.splice(index, 1) 22 | } 23 | } 24 | }) 25 | 26 | // 渲染消息 27 | render(vnode, container) 28 | document.body.appendChild(container) 29 | messageInstances.push(container) 30 | } 31 | 32 | return { 33 | info: (content, duration=700) => showMessage({ content, duration }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /接口文档.md: -------------------------------------------------------------------------------- 1 |

接口文档

2 | 3 | 4 | 5 | ## 版本信息 6 | 7 | 版本号:0.0.4 8 | 9 | 更新日期:2024-12-31 10 | 11 | 12 | 13 | ## 通用说明 14 | 15 | ### 注意事项 16 | 所有的接口皆为 promise ,需要使用 await 或者 then 进行处理 17 | 18 | 19 | ### 响应格式 20 | 21 | ```json 22 | { 23 | code: 0, 24 | data: null, 25 | message: 'success' 26 | } 27 | ``` 28 | 29 | 30 | 31 | ### 错误码说明 32 | 33 | - 0 表示操作成功 34 | 35 | - -1 表示操作失败 36 | 37 | - -2 表示用户取消了操作 38 | 39 | 40 | 41 | ### 通用参数说明 42 | 43 | 无 44 | 45 | 46 | 47 | ## 主页面 API 48 | 49 | 获取当前IP地址以及归属地信息 50 | 51 | - 事件名: `get-ip-info` 52 | 53 | - 方向: 渲染进程 → 主进程 54 | 55 | - 参数: 无 56 | 57 | - 返回值: 58 | 59 | ```json 60 | { 61 | code: 0, 62 | data:{ 63 | ip: 1.1.1.1, 64 | location: 中国-北京市-海淀区 65 | }, 66 | message: 'success' 67 | } 68 | ``` 69 | 70 | 71 | 72 | 73 | 74 | 开始记录 75 | 76 | - 事件名: `start-record` 77 | 78 | - 方向: 渲染进程 → 主进程 79 | 80 | - 参数: 无 81 | 82 | - 返回值: 83 | 84 | ```json 85 | { 86 | code: 0, 87 | data: null, 88 | message: 'success' 89 | } 90 | ``` 91 | 92 | 93 | 94 | 暂停记录 95 | 96 | - 事件名: `pause-record` 97 | 98 | - 方向: 渲染进程 → 主进程 99 | 100 | - 参数: 无 101 | 102 | - 返回值: 103 | 104 | ```json 105 | { 106 | code: 0, 107 | data: null, 108 | message: 'success' 109 | } 110 | ``` 111 | 112 | 113 | 114 | 继续记录 115 | 116 | - 事件名: `resume-record` 117 | 118 | - 方向: 渲染进程 → 主进程 119 | 120 | - 参数: 无 121 | 122 | - 返回值: 123 | 124 | ```json 125 | { 126 | code: 0, 127 | data: null, 128 | message: 'success' 129 | } 130 | ``` 131 | 132 | 133 | 134 | 停止记录 135 | 136 | - 事件名: `stop-record` 137 | 138 | - 方向: 渲染进程 → 主进程 139 | 140 | - 参数: 无 141 | 142 | - 返回值: 143 | 144 | ```json 145 | { 146 | code: 0, 147 | data: null, 148 | message: 'success' 149 | } 150 | ``` 151 | 152 | 153 | 154 | 是否存在历史记录 155 | 156 | - 事件名: `has-history` 157 | 158 | - 方向: 渲染进程 → 主进程 159 | 160 | - 参数: 无 161 | 162 | - 返回值: 163 | 164 | ```json 165 | { 166 | code: 0, 167 | data: boolean, 168 | message: 'success' 169 | } 170 | ``` 171 | 172 | 173 | 174 | 导出结果 175 | 176 | - 事件名: `export-record` 177 | 178 | - 方向: 渲染进程 → 主进程 179 | 180 | - 参数: 无 181 | 182 | - 返回值: 183 | 184 | ```json 185 | { 186 | code: 0, 187 | data: { 188 | filePath: string, 189 | counts: number, 190 | }, 191 | message: 'success' 192 | } 193 | ``` 194 | 195 | 196 | 197 | 后端向前端同步 IP 结果 198 | 199 | - 事件名: `ip-updated` 200 | 201 | - 方向: 主进程 → 渲染进程 202 | 203 | - 参数: 204 | 205 | ```json 206 | { 207 | code: 0, 208 | data:{ 209 | ip: 1.1.1.1, 210 | location: 中国-北京市-海淀区 211 | }, 212 | message: 'success' 213 | } 214 | ``` 215 | 216 | - 返回值: 无 217 | 218 | 219 | ## 设置窗口 API 220 | 221 | 测试代理可用性 222 | 223 | - 事件名: `test-proxy` 224 | 225 | - 方向: 渲染进程 → 主进程 226 | 227 | - 参数: 228 | 229 | ```json 230 | { 231 | type: 0, // 0: 无代理 、 1: 系统代理 、 2: 自定义代理 232 | data:{ 233 | protocol: 0, // 0: SOCKS5 、 1: HTTP 、 2: HTTPS 234 | addr: string, 235 | port: number, 236 | auth: { 237 | user: string, // 非必须 238 | pass: string // 非必须 239 | } 240 | }, 241 | } 242 | ``` 243 | 244 | - 返回值: 245 | 246 | ```json 247 | { 248 | code: 0, 249 | data: { 250 | available: true, // 是否可用 251 | ip: '1.2.3.4', // 测试时获取到的IP 252 | location: '中国 上海', // IP归属地 253 | }, 254 | message: 'success' 255 | } 256 | ``` 257 | 258 | 259 | 260 | 保存代理设置 261 | 262 | - 事件名: `save-proxy` 263 | 264 | - 方向: 渲染进程 → 主进程 265 | 266 | - 参数: 267 | 268 | ```json 269 | { 270 | type: 0, // 0: 无代理 、 1: 系统代理 、 2: 自定义代理 271 | data:{ 272 | protocol: 0, // 0: SOCKS5 、 1: HTTP 、 2: HTTPS 273 | addr: string, 274 | port: number, 275 | user: string, // 非必须 276 | pass: string // 非必须 277 | }, 278 | } 279 | ``` 280 | 281 | - 返回值: 282 | 283 | ```json 284 | { 285 | code: 0, 286 | data: null, 287 | message: 'success' 288 | } 289 | ``` 290 | 291 | 292 | 293 | 测试数据接口可用性 294 | 295 | - 事件名: `test-api-source` 296 | 297 | - 方向: 渲染进程 → 主进程 298 | 299 | - 参数: 300 | 301 | ```json 302 | { 303 | apiSource: 0, // 0: ipip.net 、 1: ipify 、 2: 美图 、 3: ip-api 304 | } 305 | ``` 306 | 307 | - 返回值: 308 | 309 | ```json 310 | { 311 | code: 0, 312 | data: { 313 | available: true, // 是否可用 314 | ip: '1.2.3.4', // 测试时获取到的IP 315 | location: '中国 上海', // IP归属地 316 | }, 317 | message: 'success' 318 | } 319 | ``` 320 | 321 | 322 | 323 | 保存数据接口选择 324 | 325 | - 事件名: `save-api-source` 326 | 327 | - 方向: 渲染进程 → 主进程 328 | 329 | - 参数: 330 | 331 | ```json 332 | { 333 | apiSource: 0, // 0: ipip.net 、 1: ipify 、 2: 美图 、 3: ip-api 334 | } 335 | ``` 336 | 337 | - 返回值: 338 | 339 | ```json 340 | { 341 | code: 0, 342 | data: null, 343 | message: 'success' 344 | } 345 | ``` 346 | 347 | 348 | 349 | 保存请求配置 350 | 351 | - 事件名: `save-request-options` 352 | 353 | - 方向: 渲染进程 → 主进程 354 | 355 | - 参数: 356 | 357 | ```json 358 | { 359 | times: 5, // number, 表示重复尝试次数 360 | interval: 500 // number, 表示请求时间间隔 361 | } 362 | ``` 363 | 364 | - 返回值: 365 | 366 | ```json 367 | { 368 | code: 0, 369 | data: null, 370 | message: 'success' 371 | } 372 | ``` 373 | 374 | 375 | 376 | ## 数据类型说明 377 | 378 | ### 代理类型 379 | 380 | - 0 - 无代理 381 | 382 | - 1 - 系统代理 383 | 384 | - 2 - 自定义代理 385 | 386 | 387 | 388 | ### 协议类型 389 | 390 | - 0 - SOCKS5 391 | 392 | - 1 - HTTP 393 | 394 | - 2 - HTTPS 395 | 396 | 397 | 398 | ### API来源类型 399 | 400 | - 0 - ipip.net 401 | - 1 - ipify 402 | - 2 - baidu 403 | - 3 - ip-api 404 | 405 | --------------------------------------------------------------------------------