├── .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 | {{ ip }} {{ location }} {{ proxy.name }} {{ protocolText }} {{ type.name }} 主题设置 {{ api.link }} 保存 测试
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 |
31 |
32 | 功能比较直观,左侧显示IP信息,右侧是记录的控制按钮,最右侧为工具栏,可以点击设置按钮进入设置页面
33 |
34 |
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 | 
66 |
67 | 之后在 dist 目录下就生成了打包好的程序
68 |
69 | 
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 |
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 |
110 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
53 |
184 |
192 |
51 |
52 |