Repository: vicanso/cyberapi
Branch: main
Commit: 9e0bf4499039
Files: 123
Total size: 426.1 KB
Directory structure:
gitextract_n8v88nit/
├── .eslintignore
├── .eslintrc.js
├── .gitattributes
├── .github/
│ └── workflows/
│ ├── publish.yml
│ └── test.yml
├── .gitignore
├── .vscode/
│ └── extensions.json
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── README_uk.md
├── README_zh.md
├── cliff.toml
├── dev.md
├── index.html
├── package.json
├── splashscreen.html
├── src/
│ ├── App.tsx
│ ├── commands/
│ │ ├── api_collection.ts
│ │ ├── api_folder.ts
│ │ ├── api_setting.ts
│ │ ├── cookies.ts
│ │ ├── database.ts
│ │ ├── fake.ts
│ │ ├── fn.ts
│ │ ├── http_request.ts
│ │ ├── http_response.ts
│ │ ├── import_api.ts
│ │ ├── interface.ts
│ │ ├── invoke.ts
│ │ ├── variable.ts
│ │ └── window.ts
│ ├── components/
│ │ ├── APIResponse/
│ │ │ ├── index.tsx
│ │ │ ├── list.tsx
│ │ │ └── status_text.tsx
│ │ ├── APISettingParams/
│ │ │ ├── index.tsx
│ │ │ ├── req_params.tsx
│ │ │ └── uri.tsx
│ │ ├── APISettingTree/
│ │ │ ├── header.tsx
│ │ │ ├── index.tsx
│ │ │ ├── item_dropdown.tsx
│ │ │ └── items.tsx
│ │ ├── ExColumn.tsx
│ │ ├── ExCookieEditor.tsx
│ │ ├── ExDeleteCheck.tsx
│ │ ├── ExDialog.tsx
│ │ ├── ExForm.tsx
│ │ ├── ExKeyValue.tsx
│ │ ├── ExLoading.tsx
│ │ ├── ExPreview.tsx
│ │ └── ExTimer.tsx
│ ├── constants/
│ │ ├── common.ts
│ │ ├── handle_key.ts
│ │ ├── provide.ts
│ │ └── style.ts
│ ├── env.d.ts
│ ├── event.ts
│ ├── helpers/
│ │ ├── curl.js
│ │ ├── editor.ts
│ │ ├── hot_key.ts
│ │ ├── html.ts
│ │ ├── pinyin.ts
│ │ └── util.ts
│ ├── i18n/
│ │ ├── en.ts
│ │ ├── index.ts
│ │ ├── uk.ts
│ │ └── zh.ts
│ ├── icons.ts
│ ├── main.css
│ ├── main.ts
│ ├── root.tsx
│ ├── router/
│ │ ├── index.ts
│ │ └── routes.ts
│ ├── stores/
│ │ ├── api_collection.ts
│ │ ├── api_folder.ts
│ │ ├── api_setting.ts
│ │ ├── app.ts
│ │ ├── cookie.ts
│ │ ├── dialog.ts
│ │ ├── environment.ts
│ │ ├── global_req_header.ts
│ │ ├── header.ts
│ │ ├── local.ts
│ │ ├── pin_request.ts
│ │ ├── setting.ts
│ │ └── variable.ts
│ ├── userWorker.ts
│ └── views/
│ ├── AppHeader.tsx
│ ├── AppSetting.tsx
│ ├── Collection.tsx
│ ├── CookieSetting.tsx
│ ├── Dashboard.tsx
│ ├── StoreSetting.tsx
│ └── VariableSetting.tsx
├── src-tauri/
│ ├── .gitignore
│ ├── Cargo.toml
│ ├── build.rs
│ ├── icons/
│ │ └── icon.icns
│ ├── src/
│ │ ├── commands.rs
│ │ ├── cookies.rs
│ │ ├── entities/
│ │ │ ├── api_collections.rs
│ │ │ ├── api_folders.rs
│ │ │ ├── api_settings.rs
│ │ │ ├── mod.rs
│ │ │ ├── prelude.rs
│ │ │ ├── variables.rs
│ │ │ └── versions.rs
│ │ ├── error.rs
│ │ ├── http_request.rs
│ │ ├── main.rs
│ │ ├── schemas/
│ │ │ ├── api_collection.rs
│ │ │ ├── api_folder.rs
│ │ │ ├── api_setting.rs
│ │ │ ├── database.rs
│ │ │ ├── mod.rs
│ │ │ ├── variable.rs
│ │ │ └── version.rs
│ │ └── util.rs
│ └── tauri.conf.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintignore
================================================
# don't ever lint node_modules
node_modules
# don't lint build output (make sure it's set to your correct build folder name)
dist
# don't lint nyc coverage output
coverage
.eslintrc.js
src-tauri
================================================
FILE: .eslintrc.js
================================================
module.exports = {
root: true,
parser: 'vue-eslint-parser',
plugins: [
'@typescript-eslint',
],
parserOptions: {
parser: '@typescript-eslint/parser',
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:vue/vue3-recommended',
]
};
================================================
FILE: .gitattributes
================================================
*.ts linguist-language=rust
================================================
FILE: .github/workflows/publish.yml
================================================
name: "publish"
on:
push:
branches:
- release
jobs:
publish-tauri:
strategy:
fail-fast: false
matrix:
platform: [macos-latest, ubuntu-20.04, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v3
- name: setup node
uses: actions/setup-node@v3
- name: install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-20.04'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
- name: install app dependencies and build it
run: yarn && yarn build
- uses: tauri-apps/tauri-action@v0.4.3
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
with:
tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version
releaseName: "CyberAPI v__VERSION__"
releaseBody: "See the assets to download this version and install."
prerelease: false
================================================
FILE: .github/workflows/test.yml
================================================
name: "test"
on:
push:
branches: [ main ]
jobs:
publish-tauri:
strategy:
fail-fast: false
matrix:
platform: [macos-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v3
- name: setup node
uses: actions/setup-node@v3
- name: install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: install app dependencies and build it
run: |
yarn
yarn build
cargo install tauri-cli
make build
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
stats.html
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": ["Vue.volar"]
}
================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.
## [0.1.21] - 2024-05-30
### Bug Fixes
- Fix url input symbols line break
- Fix read text from clipboard no permissions
### Miscellaneous Tasks
- Update dependencies
- Version 0.1.20
- Version 0.1.21
### Refactor
- Set overflow hidden for editor
## [0.1.20] - 2024-04-21
### Bug Fixes
- Fix url input symbols line break
### Miscellaneous Tasks
- Update dependencies
### Refactor
- Set overflow hidden for editor
## [0.1.19] - 2024-02-19
### Bug Fixes
- Fix export settings
### Refactor
- Adjust import api setting
## [0.1.18] - 2023-12-09
### Bug Fixes
- Fix select api item
### Miscellaneous Tasks
- Update dependencies
- Update dependencies
### Refactor
- Adjust menu of windows
## [0.1.17] - 2023-10-10
### Bug Fixes
- Fix expired cookie for cookie store
### Miscellaneous Tasks
- Update dependencies
## [0.1.16] - 2023-09-18
### Documentation
- Update document
### Refactor
- Update dependencies
## [0.1.15] - 2023-08-12
### Bug Fixes
- Fix dashboard scroll
- Fix content type of multipart form
### Miscellaneous Tasks
- Update dependencies
- Update dependencies
## [0.1.14] - 2023-07-10
### Bug Fixes
- Fix media query list add event listener, #27
### Features
- Support import api from swagger, #26
### Refactor
- Enhance api select handle
- Adjust zip feature for small size
## [0.1.13] - 2023-07-02
### Bug Fixes
- Fix parse file of header
### Features
- Support import from curl command
### Miscellaneous Tasks
- Update install cli script
### Refactor
- Enhance query string format
## [0.1.12] - 2023-06-29
### Bug Fixes
- Fix base64 encode, #23
### Refactor
- Improve the performance of select file
- Improve the performance of select file
## [0.1.11] - 2023-06-26
### Bug Fixes
- Fix set variable for req header
### Documentation
- Update readme
### Features
- Support expand view editor
## [0.1.10] - 2023-06-15
### Bug Fixes
- Fix column resize function
### Documentation
- Update readme
### Features
- Support automatic layout for editor
### Miscellaneous Tasks
- Update tauri version to 1.4.0
## [0.1.9] - 2023-06-13
### Features
- Add accept-encoding and support text content preview
### Refactor
- Adjust json format
## [0.1.8] - 2023-06-10
### Bug Fixes
- Fix image preview for response
### Refactor
- Ignore short interval of send request
- Use monaco instead of codemirror
- Adjust editor functions
## [0.1.7] - 2023-05-23
### Miscellaneous Tasks
- Update tauri action
### Refactor
- Adjust fs scope
## [0.1.6] - 2023-05-09
### Bug Fixes
- Fix value of cookie includes "="
- Fix get set cookie value of response header
### Features
- Support url keyword filter
- Show user agent of cyberapi
### Miscellaneous Tasks
- Update dependencies
- Update dependencies
- Update modules
### Refactor
- Add space before header value
- Adjust http timeout option
- Update modules
## [0.1.5] - 2023-01-13
### Bug Fixes
- Fix double click
- Update tauri fix repeat keydown event
### Features
- Support toggle comment for json editor
## [0.1.4] - 2022-12-06
### Bug Fixes
- Fix short cut doesn't work
- Fix generate curl function
- Fix keydown event trigger twice
### Miscellaneous Tasks
- Version 0.1.3
- Update package
## [0.1.3] - 2022-11-26
### Features
- Support select latest response
- Support hide application for mac
- Support clear history of response
- Support hide application of macos
### Refactor
- Show response of http request
## [0.1.2] - 2022-11-08
### Bug Fixes
- Fix import cancel handle
### Documentation
- Update readme
### Features
- Use tracing to get http stats
- Support request timeout setting
- Support tls tracing
- Support clear cookie function
### Miscellaneous Tasks
- Adjust publish workflow
- Update modules
### Refactor
- Remove unused todo
- Show send time of request
- Adjust splash screen
- Adjust cookie setting view
- Adjust cookie view
- Enhance api item drag function
- Try request error and convert to response
- Set auto focus mirror editor
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: Makefile
================================================
.PHONY: default
lint:
cd src-tauri && cargo clippy
fmt:
cd src-tauri && cargo fmt --all --
dev:
cargo tauri dev
icon:
cargo tauri icon ./cyberapi.png
build:
cargo tauri build
clean:
cd src-tauri && cargo clean
install-cli:
cargo install tauri-cli --version 1.4.0
install-orm-cli:
cargo install sea-orm-cli
orm:
cd src-tauri && sea-orm-cli generate entity --with-serde=both \
-u "sqlite:///~/Library/Application Support/com.bigtree.cyberapi/my_db.db" \
-o src/entities
version:
git cliff --unreleased --tag 0.1.21 --prepend CHANGELOG.md
================================================
FILE: README.md
================================================
# Cyber API
[](https://github.com/vicanso/cyberapi/actions?query=workflow%3A%22test%22)
[](https://github.com/vicanso/cyberapi)
[](https://github.com/vicanso/cyberapi/releases)
English|[简体中文](./README_zh.md)|[Українська](./README_uk.md)
## Features
- Support macos, windows and linux platforms, the installation package is below 10MB
- Thousands of interfaces for a single project are opened in seconds, with low memory usage
- Support dark/light theme and chinese/english/ukrainian
- Operation and configuration is simple
- Support importing configuration from postman, insonmia or swagger.
- The configuration can be exported by interface, function and project, which is convenient for sharing
- Support many custom functions
## Installation
The installer can be downloaded through [release](https://github.com/vicanso/cyberapi/releases), including windows, macos and linux versions.
Windows may need to install webview2.
## macos
> If you can't open it after installation, exec the following command then reopen:
`sudo xattr -rd com.apple.quarantine /Applications/CyberAPI.app`
## Development
The project depends on rust and Nodejs. If you want to compile or participate in the development yourself, you can first install the dependencies of tauri by referring to the relevant documents [here](https://tauri.app/v1/guides/getting-started/prerequisites), and then :
```shell
yarn
```
Install tauri-cli:
```shell
cargo install tauri-cli
```
Running in browser mode, the data is mock:
```shell
yarn dev
```
Running in app mode:
```shell
make dev
```
Build the release package:
```shell
make build
```
================================================
FILE: README_uk.md
================================================
# Cyber API
[](https://github.com/vicanso/cyberapi/actions?query=workflow%3A%22test%22)
[](https://github.com/vicanso/cyberapi)
[](https://github.com/vicanso/cyberapi/releases)
CyberAPI це інструмент для запитів до API оснований на tauri.
[English](./README.md)|[简体中文](./README_zh.md)|Українська
## Features
- Підтримка платформ macos, windows і linux, інсталяційний пакет менше 10 МБ
- Тисячі інтерфейсів для одного проекту відкриваються за лічені секунди, з низьким споживанням пам'яті
- Підтримка темної / світлої теми та китайської / англійської / української мови
- Проста експлуатація та конфігурація
- Підтримка імпорту конфігурації з postman, insonmia або swagger.
- Конфігурацію можна експортувати за інтерфейсом, функцією та проектом, що зручно для спільного використання
- Підтримка багатьох користувацьких функцій
## Installation
Інсталятор можна завантажити з [release](https://github.com/vicanso/cyberapi/releases), включаючи версії для Windows, macos і linux.
У Windows може знадобитися встановити webview2.
## Development
Проект залежить від rust та Nodejs. Якщо ви хочете самостійно компілювати або брати участь у розробці, ви можете спочатку встановити залежності tauri, звернувшись до відповідних документів [тут](https://tauri.app/v1/guides/getting-started/prerequisites), а потім :
```shell
yarn
```
Встановити tauri-cli:
```shell
cargo install tauri-cli
```
При запуску в режимі браузера дані є імітацією:
```shell
yarn dev
```
Запустити в режимі додатку:
```shell
make dev
```
Зібрати релізний пакет:
```shell
make build
```
================================================
FILE: README_zh.md
================================================
# Cyber API
[](https://github.com/vicanso/cyberapi/actions?query=workflow%3A%22test%22)
[](https://github.com/vicanso/cyberapi)
[English](./README.md)|简体中文|[Українська](./README_uk.md)
## 功能点
- 支持macos、windows以及linux平台,安装包均在10MB以下
- 单个项目上千个接口秒级打开,内存占用较低
- 支持Dark/Light主题以及中英语言
- 简单易用的操作及配置方式
- 可快速导入postman,insomnia或者swagger的配置
- 关键字筛选支持中文拼音或者首字母
- 可按接口、按功能、按项目导出配置,方便团队内共用
- 各类自定义的函数,方便各请求间关联数据
CyberAPI暂时仅是开发版本,业余时间的个人项目,如果有BUG或期望新增功能可以issue,对于BUG请附上系统版本信息,本人尽可能抽时间处理。
## 安装
安装程序可以通过[release](https://github.com/vicanso/cyberapi/releases)下载,包括windows、macos以及linux版本。
需要注意如果是win7或者未安装Edge的windows,在安装时会提示需要执行MicrosoftEdgeUpdateSetup的程序,如果杀毒软件提示允许执行即可。
如果是macos,由于系统的安全调整,打开应用时会提示"无法打开“CyberAPI”,因为Apple无法检查其是否包含恶意软件。",在“系统设置” -> “安全性与隐私” -> “通用”面板选择继续打开即可。或者执行以下命令:`sudo xattr -rd com.apple.quarantine /Applications/CyberAPI.app`
## 开发者
项目依赖于rust与Nodejs,如果想自行编译或参与开发,可以先参考[这里](https://tauri.app/v1/guides/getting-started/prerequisites)的相关文档安装tauri的依赖,之后执行:
```shell
yarn
```
安装tauri-cli:
```shell
cargo install tauri-cli
```
仅调整前端界面时可直接使用浏览器的方式来测试(增加了各类mock的接口),执行:
```shell
yarn dev
```
如果以APP的形式运行,则执行:
```shell
make dev
```
如果想编译安装包,则执行:
```shell
make build
```
## 创建项目
首次启动后,需要先创建项目,建议按不同的项目来创建,同一项目可共用环境变量的配置。
## 创建项目的环境变量
环境变量主要用于定义HTTP请求的host等场景,用于方便快捷的切换请求对接的环境。
tiny配置了两个环境的ENV设置,其中`http://tiny.npmtrend.com`未生效(复选框未勾选),如果需要切换不同的环境时,选择勾选不同的配置生效即可,需要注意不要同时选择相同的环境变量生效。
## 创建项目的变量
项目中使用的变量可用于各请求中的参数设置,通过`{{value(key)}}`函数引用。
## 创建目录与请求
创建请求之前,建议按功能来创建不同的分组,例如创建用户相关的一个分组:
在创建分组之后,则可以在该分组下创建对应的请求:
在创建请求之后,则可以选择请求使用的env(自动添加至请求url中),对应的HTTP Method以及输入URL。对于POST类请求的body部分,则可以选择对应的数据类型,如选择了json数据,填写对应的参数,图中的`{{md5(123123)}}`为函数形式,会在请求时执行此函数,填充对应的数据,后续会专门介绍此类函数:
配置完成后,点击发送则发送该请求,获取到响应后则展示如下图。第一个图标点击时会展示该请求的ID(后续可用于其它请求指定获取该请求的响应时使用),第二个图标点击会展示此请求对应的`curl`。
### 不同的数据类型
数据提交较为常用的是JSON以及Form类型,下图示例为选择Form类型的数据填写(需要注意切换数据类型时会清除原有数据)。如果临时想不使用某个参数,则可取消勾选即可,不需要的则可以删除,参数也可使用函数的形式:
`Query`与`Header`的设置与`Form`类似,不再细说。
## Pin
对于经常使用的请求,可以通过`Pin`操作将其置顶,便于后续使用,操作如下:
## 内置函数
CyberAPI内置支持了部分函数,便于在请求中动态生成参数值,具体函数如下:
- `readTextFile`:读取文本文件,也可使用简写`rtf`
- `readFile`:读取文件,也可使用简写`rf`
- `base64`:转换为base64,也可使用简写`b64`
- `openFile`:打开文件,弹窗让用户选择要打开的文件,也可使用简写`of`
- `get`:获取请求响应数据的值,也可使用简写`g`
- `timestamp`:时间戳(秒),也可使用简写`ts`
- `md5`:md5并以hex形式输出
- `sha256`:sha256并以hex形式输出
### readTextFile
读取文件并以字符的形式返回,其参数为`(file, dir)`,其中`dir`为可选参数,默认为`Download`目录,基于安全考虑,仅允许使用三个目录:`document`,`desktop`以及`download`。例如读取`download`目录下的`test/data.json`,其使用方式:`{{readTextFile(test/data.json, download)}}`
### readFile
读取文件并以字节的形式返回,其参数及使用方法与`readTextFile`一致,仅返回数据的类型不一样
### base64
将字节转换为base64的形式,一般用于函数串联时使用,如读取图片并将其转换为base64,使用方式:`{{base64.readFile(test-files/data.jpg, document)}}`,表示读取图片后,将图片的数据转换为bas64。函数串联可以以多个函数串联使用,处理方式为由右往左,后一个函数参数为前一个函数的返回。
### openFile
弹出文件打开选择框,由客户选择要打开的文件,其返回客户选择的文件路径。
### get
获取指定请求的响应,例如A请求的响应为`{"data": {"name": "CyberAPI"}}`,B请求需要使用A请求的响应的`data.name`,则先获取请求A的ID,在请求A的响应中点图标获取,如下:
在得到请求A的ID之后(该ID不会变化),则使用方式:`{{get(01GCE5X1X5FXM3D13NQQKYEWF7, data.name)}}`,此方式主要用于接口依赖于其它接口的响应的场景中使用。
注:如果响应数据data为数组,从数组中获取第一个元素的值,则参数如下:`data.0.name`
### timestamp
时间戳(秒),用于返回当前的时间,使用方式:`{{timestamp()}}`
### md5/sha256
计算md5并以hex形式输出,使用方式:`{{md5(123123)`,sha256的使用方式一致。
### value
获取全局配置的值,使用方式`{{value(key)}}`,方便获取项目中配置的变量数据。
## Cookie设置
Cookie的数据为应用共享,在HTTP响应头中有`Set-Cookie`则会自动保存,需要注意对于`Session`有效期的Cookie,在程序关闭之后会自动清除。用户可直接修改Cookie的有效期、值等信息或删除Cookie。
## 配置导入
配置导入支持四种方式,`JSON`与`File`仅支持`CyberAPI`的配置形式导入,`PostMan`用于导入postman的配置,`Insonmia`则用于导入insonmia的配置。
================================================
FILE: cliff.toml
================================================
# configuration file for git-cliff (0.1.0)
[changelog]
# changelog header
header = """
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://tera.netlify.app/docs/#introduction
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
"""
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/orhun/git-cliff/issues/${2}))"},
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "Features"},
{ message = "^fix", group = "Bug Fixes"},
{ message = "^doc", group = "Documentation"},
{ message = "^perf", group = "Performance"},
{ message = "^refactor", group = "Refactor"},
{ message = "^style", group = "Styling"},
{ message = "^test", group = "Testing"},
{ message = "^chore\\(release\\): prepare for", skip = true},
{ message = "^chore", group = "Miscellaneous Tasks"},
{ body = ".*security", group = "Security"},
]
# filter out the commits that are not matched by commit parsers
filter_commits = false
# glob pattern for matching git tags
tag_pattern = "v[0-9]*"
# regex for skipping tags
skip_tags = "v0.1.0-beta.1"
# regex for ignoring tags
ignore_tags = ""
# sort the tags chronologically
date_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"
================================================
FILE: dev.md
================================================
# 查询证件列表
```
security find-identity -v -p codesigning
```
# 校验IMG
校验IMG是否使用证件签名:
```
spctl -a -v src-tauri/target/release/bundle/dmg/cyberapi_0.1.0_aarch64.dmg
```
# 版本发布
修改版本号后执行`make version`生成修改记录,提交代码后合并至release
================================================
FILE: index.html
================================================
CyberAPI
================================================
FILE: package.json
================================================
{
"name": "cyberapi",
"private": true,
"version": "",
"scripts": {
"format": "prettier --write src/*.ts src/*.tsx src/**/*.ts src/**/*.tsx src/**/**/*.tsx",
"lint": "eslint . --ext .js,.tsx,.ts --fix && vue-tsc --noEmit",
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@linaria/core": "3.0.0-beta.22",
"@tauri-apps/api": "^1.5.3",
"@tauri-apps/cli": "^1.5.11",
"@vicons/ionicons5": "^0.12.0",
"bluebird": "^3.7.2",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.10",
"debug": "^4.3.4",
"form-data-encoder": "^4.0.2",
"js-base64": "^3.7.7",
"localforage": "^1.10.0",
"lodash-es": "^4.17.21",
"mime": "^4.0.1",
"mitt": "^3.0.1",
"monaco-editor": "^0.47.0",
"naive-ui": "^2.38.1",
"pinia": "^2.1.7",
"pretty-bytes": "^6.1.1",
"shellwords": "^1.0.1",
"ulid": "^2.3.0",
"vue": "^3.4.21",
"vue-i18n": "^9.11.1",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@types/bluebird": "^3.5.42",
"@types/crypto-js": "^4.2.2",
"@types/debug": "^4.1.12",
"@types/mime": "^3.0.4",
"@types/shortid": "^0.0.32",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^7.6.0",
"@typescript-eslint/parser": "^7.6.0",
"@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"@vue/compiler-sfc": "^3.4.21",
"eslint": "^9.0.0",
"eslint-plugin-vue": "^9.24.1",
"glob-parent": ">=6.0.2",
"prettier": "^3.2.5",
"rollup-plugin-visualizer": "^5.12.0",
"typescript": "^5.4.4",
"vite": "^5.2.8",
"vite-plugin-linaria": "^1.0.0",
"vue-tsc": "^2.0.11"
}
}
================================================
FILE: splashscreen.html
================================================
CyberAPI
Cyber API
================================================
FILE: src/App.tsx
================================================
import { defineComponent, onMounted } from "vue";
import { NLayout, NLayoutHeader, useLoadingBar, NModal } from "naive-ui";
import { storeToRefs } from "pinia";
import "./main.css";
import AppHeader from "./views/AppHeader";
import { useDialogStore } from "./stores/dialog";
import AppSetting from "./views/AppSetting";
import CookieSetting from "./views/CookieSetting";
import VariableSetting from "./views/VariableSetting";
import StoreSetting from "./views/StoreSetting";
import { VariableCategory } from "./commands/variable";
import {
i18nEnvironment,
i18nCustomizeVariable,
i18nGlobalReqHeader,
} from "./i18n";
export default defineComponent({
name: "App",
setup() {
const loadingBar = useLoadingBar();
const dialogStore = useDialogStore();
const {
showSetting,
showCookie,
showEnvironment,
showStore,
showCustomizeVariableStore,
showReqHeader,
} = storeToRefs(dialogStore);
const closeDialog = () => {
dialogStore.$reset();
};
onMounted(() => {
loadingBar.finish();
});
return {
closeDialog,
showSetting,
showCookie,
showEnvironment,
showStore,
showCustomizeVariableStore,
showReqHeader,
};
},
render() {
const {
showSetting,
showCookie,
showEnvironment,
showStore,
showCustomizeVariableStore,
showReqHeader,
closeDialog,
} = this;
const settingModal = (
{
closeDialog();
}}
onMaskClick={() => {
closeDialog();
}}
>
);
const cookieModal = (
{
closeDialog();
}}
onMaskClick={() => {
closeDialog();
}}
>
);
const environmentModal = (
{
closeDialog();
}}
onMaskClick={() => {
closeDialog();
}}
>
);
const customizeVariableModal = (
{
closeDialog();
}}
onMaskClick={() => {
closeDialog();
}}
>
);
const reqHeaderModal = (
{
closeDialog();
}}
onMaskClick={() => {
closeDialog();
}}
>
);
const storeModal = (
{
closeDialog();
}}
onMaskClick={() => {
closeDialog();
}}
>
);
return (
{settingModal}
{cookieModal}
{environmentModal}
{storeModal}
{customizeVariableModal}
{reqHeaderModal}
);
},
});
================================================
FILE: src/commands/api_collection.ts
================================================
import dayjs from "dayjs";
import { ulid } from "ulid";
import { isWebMode } from "../helpers/util";
import { fakeAdd, fakeDeleteAPICollection, fakeList, fakeUpdate } from "./fake";
import {
run,
cmdAddAPICollection,
cmdListAPICollection,
cmdUpdateAPICollection,
cmdDeleteAPICollection,
} from "./invoke";
const store = "apiCollections";
export interface APICollection {
[key: string]: unknown;
id: string;
// 名称
name: string;
// 描述
description: string;
// 创建时间
createdAt: string;
// 更新时间
updatedAt: string;
}
export function newDefaultAPICollection(): APICollection {
const id = ulid();
return {
id,
name: "",
description: "",
createdAt: dayjs().format(),
updatedAt: dayjs().format(),
};
}
export async function createAPICollection(
collection: APICollection,
): Promise {
if (isWebMode()) {
await fakeAdd(store, collection);
return;
}
await run(cmdAddAPICollection, {
collection,
});
}
export async function listAPICollection(): Promise {
if (isWebMode()) {
return await fakeList(store);
}
return await run(cmdListAPICollection);
}
export async function updateAPICollection(collection: APICollection) {
if (isWebMode()) {
await fakeUpdate(store, collection);
return;
}
await run(cmdUpdateAPICollection, {
collection,
});
}
export async function deleteAPICollection(id: string) {
if (isWebMode()) {
await fakeDeleteAPICollection(store, id);
}
await run(cmdDeleteAPICollection, {
id,
});
}
================================================
FILE: src/commands/api_folder.ts
================================================
import dayjs from "dayjs";
import { ulid } from "ulid";
import { isWebMode } from "../helpers/util";
import {
run,
cmdAddAPIFolder,
cmdListAPIFolder,
cmdUpdateAPIFolder,
cmdDeleteAPIFolder,
} from "./invoke";
import { fakeList, fakeAdd, fakeUpdate, fakeDeleteItems } from "./fake";
const store = "apiFolders";
export interface APIFolder {
[key: string]: unknown;
id: string;
collection: string;
children: string;
// 名称
name: string;
// 创建时间
createdAt: string;
// 更新时间
updatedAt: string;
}
export function newDefaultAPIFolder(): APIFolder {
const id = ulid();
return {
id,
collection: "",
children: "",
name: "",
createdAt: dayjs().format(),
updatedAt: dayjs().format(),
};
}
export async function createAPIFolder(folder: APIFolder): Promise {
if (isWebMode()) {
await fakeAdd(store, folder);
return;
}
await run(cmdAddAPIFolder, {
folder,
});
}
export async function listAPIFolder(collection: string): Promise {
if (isWebMode()) {
const folders = await fakeList(store);
return folders.filter((item) => item.collection === collection);
}
return await run(cmdListAPIFolder, {
collection,
});
}
export async function updateAPIFolder(folder: APIFolder) {
if (isWebMode()) {
return await fakeUpdate(store, folder);
}
await run(cmdUpdateAPIFolder, {
folder,
});
}
export async function deleteAPIFolder(id: string): Promise<{
folders: string[];
settings: string[];
}> {
const result = {
folders: [] as string[],
settings: [] as string[],
};
if (isWebMode()) {
// 查询folders
const folders = await listAPIFolder("");
const folderDict: Map = new Map();
folders.forEach((item) => {
folderDict.set(item.id, item);
});
if (!folderDict.has(id)) {
return Promise.resolve(result);
}
const folderIds = [id];
const settingIds: string[] = [];
let children = folderDict.get(id)?.children;
while (children) {
const subChildren: string[] = [];
const arr = children.split(",");
arr.forEach((id) => {
if (!id) {
return;
}
if (settingIds.includes(id) || folderIds.includes(id)) {
return;
}
const folder = folderDict.get(id);
if (folder) {
folderIds.push(id);
if (folder.children) {
subChildren.push(folder.children);
}
} else {
settingIds.push(id);
}
});
children = subChildren.join(",");
}
await fakeDeleteItems("apiSettings", settingIds);
await fakeDeleteItems(store, folderIds);
result.folders = folderIds;
result.settings = settingIds;
return result;
}
return await run(cmdDeleteAPIFolder, {
id,
});
}
================================================
FILE: src/commands/api_setting.ts
================================================
import dayjs from "dayjs";
import { ulid } from "ulid";
import { isWebMode } from "../helpers/util";
import {
run,
cmdAddAPISetting,
cmdListAPISetting,
cmdUpdateAPISetting,
cmdDeleteAPISettings,
} from "./invoke";
import { fakeList, fakeAdd, fakeUpdate, fakeUpdateStore } from "./fake";
const store = "apiSettings";
export interface APISetting {
[key: string]: unknown;
id: string;
collection: string;
// 名称
name: string;
// 类型(http, graphQL)
category: string;
// 配置信息
setting: string;
// 创建时间
createdAt: string;
// 更新时间
updatedAt: string;
}
export function newDefaultAPISetting(): APISetting {
const id = ulid();
return {
id,
collection: "",
name: "",
category: "",
setting: "",
createdAt: dayjs().format(),
updatedAt: dayjs().format(),
};
}
export async function createAPISetting(setting: APISetting): Promise {
if (isWebMode()) {
await fakeAdd(store, setting);
return;
}
if (!setting.id) {
setting.id = ulid();
}
try {
await run(cmdAddAPISetting, {
setting,
});
} catch (err) {
let catchError = false;
if (err instanceof Error) {
const message = err.message;
if (message.includes("seaOrm") && message.includes("UNIQUE constraint failed")) {
catchError = true;
setting.id = ulid();
await run(cmdAddAPISetting, {
setting,
});
}
}
if (!catchError) {
throw err;
}
}
}
export async function listAPISetting(
collection: string,
): Promise {
if (isWebMode()) {
const settings = await fakeList(store);
return settings.filter((item) => item.collection === collection);
}
return await run(cmdListAPISetting, {
collection,
});
}
export async function updateAPISetting(setting: APISetting) {
if (isWebMode()) {
await fakeUpdate(store, setting);
return;
}
await run(cmdUpdateAPISetting, {
setting,
});
}
export async function deleteAPISettings(ids: string[]) {
if (isWebMode()) {
const arr = await fakeList(store);
const result = arr.filter((item) => {
return !ids.includes(item.id);
});
await fakeUpdateStore(store, result);
}
await run(cmdDeleteAPISettings, {
ids,
});
}
================================================
FILE: src/commands/cookies.ts
================================================
import { get, values } from "lodash-es";
import { isWebMode } from "../helpers/util";
import {
cmdAddCookie,
cmdClearCookie,
cmdDeleteCookie,
cmdListCookie,
run,
} from "./invoke";
export interface Cookie {
[key: string]: unknown;
name: string;
value: string;
path: string;
domain: string;
expires: string;
}
export async function listCookie(): Promise {
if (isWebMode()) {
return Promise.resolve([
{
name: "cybertect",
value: "CBBVJIUT8Q9EEFDKF9H0",
path: "/",
domain: "cybertect.npmtrend.com",
expires: "Wed, 31 Jan 2024 16:00:00 GMT",
},
{
name: "cybertect.sig",
value: "iIoKqqpgXc-Ao-ilTf4XdaNyblsdKauy0fVqISbikoU",
path: "/",
domain: "",
expires: "Wed, 31 Jan 2024 16:00:00 GMT",
},
]);
}
const arr = await run(cmdListCookie, {});
if (!arr || !arr.length) {
return [];
}
const cookies: Cookie[] = [];
arr.forEach((data) => {
const item = JSON.parse(data);
const cookie = get(item, "raw_cookie");
if (!cookie) {
return;
}
const cookieValues = (cookie as string).split(";");
const [name, ...value] = cookieValues[0].split("=");
const path = (get(item, "path.0") || "/") as string;
const domainValues = values(get(item, "domain"));
let domain = "";
if (domainValues && domainValues.length) {
domain = domainValues[0];
}
const expires = get(item, "expires.AtUtc") as string;
cookies.push({
name,
value: value.join("="),
path: path || "",
domain: domain || "",
expires: expires || "",
});
});
return cookies;
}
export async function deleteCookie(c: Cookie) {
if (isWebMode()) {
return;
}
await run(cmdDeleteCookie, {
c,
});
}
export async function clearCookie() {
if (isWebMode()) {
return;
}
await run(cmdClearCookie, {});
}
export async function addOrUpdate(c: Cookie) {
if (isWebMode()) {
return;
}
await run(cmdAddCookie, {
c,
});
}
================================================
FILE: src/commands/database.ts
================================================
import { getVersion } from "@tauri-apps/api/app";
import { message } from "@tauri-apps/api/dialog";
import dayjs from "dayjs";
import { ulid } from "ulid";
import { isWebMode } from "../helpers/util";
import {
cmdAddVersion,
cmdExportTables,
cmdGetLatestVersion,
cmdImportTables,
cmdInitTables,
run,
} from "./invoke";
export interface Version {
[key: string]: unknown;
id: string;
version: string;
createdAt: string;
updatedAt: string;
}
async function getDatabaseLatestVersion() {
if (isWebMode()) {
return {} as Version;
}
return await run(cmdGetLatestVersion, {});
}
// handleDatabaseCompatible 处理数据库兼容
export async function handleDatabaseCompatible() {
if (isWebMode()) {
return;
}
try {
await run(cmdInitTables);
const version = await getVersion();
const latestVersion = await getDatabaseLatestVersion();
if (!latestVersion || latestVersion.version !== version) {
await run(cmdAddVersion, {
version: {
id: ulid(),
version,
createdAt: dayjs().format(),
updatedAt: dayjs().format(),
},
});
}
// TODO 后续针对数据库做更新
} catch (err) {
if (err instanceof Error) {
message(err.message);
}
console.error(err);
}
}
export async function exportTables(): Promise {
return await run(cmdExportTables);
}
export async function importTables(file: string) {
return await run(cmdImportTables, {
file,
});
}
================================================
FILE: src/commands/fake.ts
================================================
import localforage from "localforage";
const stores = new Map();
interface WithID {
id: string;
}
function getStore(name: string): LocalForage {
let store = stores.get(name);
if (store) {
return store;
}
store = localforage.createInstance({
name,
});
stores.set(name, store);
return store;
}
export async function fakeList(storeName: string): Promise {
const store = getStore(storeName);
const result = await store.getItem("fake");
if (result != null) {
return result;
}
return [];
}
export async function fakeAdd(storeName: string, data: T) {
const result = await fakeList(storeName);
result.push(Object.assign({}, data));
await fakeUpdateStore(storeName, result);
}
export async function fakeUpdate(storeName: string, data: T) {
const result = await fakeList(storeName);
let found = -1;
result.forEach((item, index) => {
if (item.id == data.id) {
found = index;
}
});
if (found !== -1) {
result[found] = Object.assign({}, data);
}
await fakeUpdateStore(storeName, result);
}
export async function fakeDeleteAPICollection(
storeName: string,
id: string,
) {
// 暂时简单删除collection
const result = await fakeList(storeName);
let found = -1;
result.forEach((item, index) => {
if (item.id == id) {
found = index;
}
});
if (found !== -1) {
result.splice(found, 1);
}
await fakeUpdateStore(storeName, result);
}
export async function fakeDeleteItems(
storeName: string,
ids: string[],
) {
const result = await fakeList(storeName);
const arr = [] as unknown[];
result.forEach((item) => {
if (!ids.includes(item.id)) {
arr.push(item);
}
});
await fakeUpdateStore(storeName, arr);
}
export async function fakeUpdateStore(storeName: string, data: unknown) {
const store = getStore(storeName);
await store.setItem("fake", data);
}
================================================
FILE: src/commands/fn.ts
================================================
import {
BaseDirectory,
FsOptions,
readBinaryFile,
readTextFile,
} from "@tauri-apps/api/fs";
import { open } from "@tauri-apps/api/dialog";
import { toString, get, trim } from "lodash-es";
import { fromUint8Array } from "js-base64";
import sha256 from "crypto-js/sha256";
import md5 from "crypto-js/md5";
import { i18nCommon } from "../i18n";
import { getLatestResponse, getResponseBody } from "./http_response";
import { listVariable, VariableCategory, VariableStatus } from "./variable";
interface FnHandler {
collection: string;
// 原始字符
text: string;
// 函数列表
fnList: string[];
// 初始参数
param: string | string[];
}
enum Fn {
readTextFile = "readTextFile",
rtf = "rtf",
readFile = "readFile",
rf = "rf",
base64 = "base64",
b64 = "b64",
openFile = "openFile",
of = "of",
get = "get",
g = "g",
timestamp = "timestamp",
ts = "ts",
md5 = "md5",
sha256 = "sha256",
value = "value",
}
function trimParam(param: string): string | string[] {
const arr = param.split(",").map((item) => {
item = item.trim();
item = trim(item, "'");
item = trim(item, '"');
return item;
});
// 单引号替换为双引号
// const str = `[${param.replaceAll("'", '"')}]`;
// const arr = JSON.parse(str);
if (arr.length < 2) {
return arr[0];
}
return arr;
}
export function parseFunctions(collection: string, value: string): FnHandler[] {
const reg = /\{\{([\s\S]+?)\}\}/g;
const parmaReg = /\(([\s\S]*?)\)/;
let result: RegExpExecArray | null;
const handlers: FnHandler[] = [];
while ((result = reg.exec(value)) !== null) {
if (result.length !== 2) {
break;
}
const paramResult = parmaReg.exec(result[1]);
if (paramResult?.length !== 2) {
break;
}
const fnList = result[1].replace(paramResult[0], "").split(".");
handlers.push({
collection,
text: result[0],
fnList: fnList,
param: trimParam(paramResult[1]),
});
}
return handlers;
}
interface FsParams {
file: string;
option: FsOptions;
}
function getDir(dir: string): BaseDirectory {
switch (dir.toLowerCase()) {
case "document":
return BaseDirectory.Document;
break;
case "desktop":
return BaseDirectory.Desktop;
break;
default:
return BaseDirectory.Download;
break;
}
}
function convertToFsParams(p: unknown): FsParams {
const option = {
dir: BaseDirectory.Download,
};
let file = toString(p);
if (Array.isArray(p)) {
file = toString(p[0]);
if (p[1]) {
option.dir = getDir(p[1]);
}
}
return {
file,
option,
};
}
export async function doFnHandler(handler: FnHandler): Promise {
const { param, fnList, collection } = handler;
let p: unknown = param;
const size = fnList.length;
// 函数处理从后往前
for (let index = size - 1; index >= 0; index--) {
const fn = fnList[index];
switch (fn) {
case Fn.readTextFile:
case Fn.rtf:
{
const params = convertToFsParams(p);
p = await readTextFile(params.file, params.option);
}
break;
case Fn.md5:
p = md5(toString(p)).toString();
break;
case Fn.sha256:
p = sha256(toString(p)).toString();
break;
case Fn.readFile:
case Fn.rf:
{
const params = convertToFsParams(p);
p = await readBinaryFile(params.file, params.option);
}
break;
case Fn.base64:
case Fn.b64:
{
p = fromUint8Array(p as Uint8Array);
}
break;
case Fn.openFile:
case Fn.of:
{
const selected = await open({
title: i18nCommon("selectFile"),
});
if (selected) {
p = selected as string;
}
}
break;
case Fn.timestamp:
case Fn.ts:
{
p = `${Math.round(Date.now() / 1000)}`;
}
break;
case Fn.get:
case Fn.g:
{
const arr = toString(p).split(",");
if (arr.length !== 2) {
throw new Error("params of get from response is invalid");
}
const resp = await getLatestResponse(arr[0].trim());
if (resp) {
const result = getResponseBody(resp);
p = get(result.json, arr[1].trim());
}
}
break;
case Fn.value:
{
const name = toString(p);
const arr = await listVariable(
collection,
VariableCategory.Customize,
);
const found = arr.find(
(item) =>
item.enabled === VariableStatus.Enabled && item.name === name,
);
if (found) {
p = found.value;
}
}
break;
default:
break;
}
}
return toString(p);
}
================================================
FILE: src/commands/http_request.ts
================================================
import { forEach, isArray } from "lodash-es";
import { encode } from "js-base64";
import { ulid } from "ulid";
import { getVersion, getTauriVersion } from "@tauri-apps/api/app";
import { arch, type, version } from "@tauri-apps/api/os";
import { FormDataEncoder } from "form-data-encoder";
import { fromUint8Array } from "js-base64";
import { readBinaryFile } from "@tauri-apps/api/fs";
import { run, cmdDoHTTPRequest } from "./invoke";
import { KVParam } from "./interface";
import { isWebMode, delay, formatError } from "../helpers/util";
import { doFnHandler, parseFunctions } from "./fn";
import { HTTPResponse, addLatestResponse } from "./http_response";
import { Cookie } from "./cookies";
import mime from "mime";
export enum HTTPMethod {
GET = "GET",
POST = "POST",
PUT = "PUT",
PATCH = "PATCH",
DELETE = "DELETE",
OPTIONS = "OPTIONS",
HEAD = "HEAD",
}
export enum ContentType {
JSON = "application/json",
Form = "application/x-www-form-urlencoded",
Multipart = "multipart/form-data",
XML = "application/xml",
Plain = "text/plain",
}
export interface RequestTimeout {
[key: string]: unknown;
connect: number;
write: number;
read: number;
}
export interface HTTPRequest {
[key: string]: unknown;
method: string;
uri: string;
body: string;
contentType: string;
headers: KVParam[];
query: KVParam[];
auth: KVParam[];
}
function convertKVListToURLValues(kvList: KVParam[]) {
if (!kvList || kvList.length === 0) {
return [];
}
const arr: string[] = [];
kvList.forEach((kv) => {
if (!kv.enabled) {
return;
}
arr.push(`${kv.key}=${encodeURIComponent(kv.value)}`);
});
return arr;
}
export async function convertRequestToCURL(
collection: string,
req: HTTPRequest,
cookies: Cookie[],
) {
await convertKVParams(collection, req.query);
await convertKVParams(collection, req.headers);
const queryList = convertKVListToURLValues(req.query);
let uri = req.uri;
if (queryList.length !== 0) {
if (uri.includes("?")) {
uri += `&${queryList.join("&")}`;
} else {
uri += `?${queryList.join("&")}`;
}
}
const headerList: string[] = [];
const host = new URL(uri).host;
const cookieValues: string[] = [];
cookies.forEach((item) => {
if (host.includes(item.domain)) {
cookieValues.push(`${item.name}=${item.value}`);
}
});
if (cookieValues.length) {
headerList.push(`-H 'Cookie: ${cookieValues.join("; ")}'`);
}
let includeContentType = false;
req.headers?.forEach((kv) => {
if (!kv.enabled) {
return;
}
if (kv.key.toLowerCase() === "content-type") {
includeContentType = true;
}
headerList.push(`-H '${kv.key}: ${kv.value}'`);
});
if (!includeContentType && req.contentType) {
headerList.push(`-H 'Content-Type: ${req.contentType}'`);
}
let body = " ";
if (req.body) {
body = await convertBody(collection, req.body);
switch (req.contentType) {
case ContentType.JSON:
body = JSON.stringify(JSON.parse(body));
break;
case ContentType.Form:
{
const arr: KVParam[] = JSON.parse(body);
body = convertKVListToURLValues(arr).join("&");
}
break;
default:
break;
}
body = ` -d '${body}' `;
}
const method = req.method || "GET";
return `curl -v -X${method.toUpperCase()}${body}${headerList.join(
" ",
)} '${uri}'`;
}
function is_json(str: string) {
const value = str.trim();
if (value.length < 2) {
return false;
}
const key = value[0] + value[value.length - 1];
return key === "{}" || key === "[]";
}
async function convertBody(collection: string, data: string) {
let body = data;
// 注释的处理
if (is_json(body)) {
const arr = body.split("\n").filter((item) => {
if (item.trim().startsWith("//")) {
return false;
}
return true;
});
body = arr.join("\n");
}
const handlers = parseFunctions(collection, body);
if (handlers.length === 0) {
return body;
}
for (let i = 0; i < handlers.length; i++) {
const handler = handlers[i];
const result = await doFnHandler(handler);
// 替换result的内容
body = body.replace(handler.text, result);
}
return body;
}
export async function convertKVParams(collection: string, params: KVParam[]) {
if (!params || params.length === 0) {
return;
}
for (let i = 0; i < params.length; i++) {
const param = params[i];
const handlers = parseFunctions(collection, param.value);
if (handlers.length === 0) {
continue;
}
let { value } = param;
for (let j = 0; j < handlers.length; j++) {
const handler = handlers[j];
const result = await doFnHandler(handler);
// 替换result的内容
value = value.replace(handler.text, result);
}
param.value = value;
}
}
export const abortRequestID = ulid();
interface MultipartFormData {
headers: {
"Content-Type": string;
"Content-Length"?: string;
};
body: string;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
async function convertMultipartForm(body: string): Promise {
const arr = JSON.parse(body) as KVParam[];
const form = new FormData();
for (let i = 0; i < arr.length; i++) {
const item = arr[i];
if (!item.enabled || !item.key) {
continue;
}
const fileProtocol = "file://";
if (item.value.startsWith(fileProtocol)) {
const file = item.value.substring(fileProtocol.length);
const fileData = await readBinaryFile(file);
form.append(
item.key,
new Blob([fileData], {
type: mime.getType(file) || "",
}),
file,
);
continue;
}
form.append(item.key, item.value);
}
// eslint-disable-next-line
// @ts-ignore
const encoder = new FormDataEncoder(form);
// eslint-disable-next-line
// @ts-ignore
const b = new Blob(encoder, {
type: encoder.contentType,
});
const buf = await b.arrayBuffer();
return {
headers: encoder.headers,
body: fromUint8Array(new Uint8Array(buf)),
};
}
export async function getUserAgent() {
const appVersion = await getVersion();
const appOS = await type();
const appOSVersion = await version();
const appArch = await arch();
const tauriVersion = await getTauriVersion();
return `CyberAPI/${appVersion} (${appOS}; tauri/${tauriVersion}; ${appOSVersion}; ${appArch})`;
}
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko)
let userAgent = "";
export async function doHTTPRequest(options: {
id: string;
collection: string;
req: HTTPRequest;
originalReq: HTTPRequest;
timeout: RequestTimeout;
}): Promise {
const { id, collection, req, originalReq, timeout } = options;
if (!req.headers) {
req.headers = [];
}
if (!req.query) {
req.query = [];
}
if (!req.auth) {
req.auth = [];
}
const method = req.method || HTTPMethod.GET;
let body = req.body || "";
let contentType = req.contentType || "";
// 非此类请求,将body设置为空
if (
![HTTPMethod.POST, HTTPMethod.PATCH, HTTPMethod.PUT].includes(
method as HTTPMethod,
)
) {
body = "";
contentType = "";
}
body = await convertBody(collection, body);
// 如果是form
if (body && contentType === ContentType.Form) {
const arr = JSON.parse(body) as KVParam[];
const result: string[] = [];
arr.forEach((item) => {
if (!item.enabled) {
return;
}
result.push(
`${window.encodeURIComponent(item.key)}=${window.encodeURIComponent(
item.value,
)}`,
);
});
body = result.join("&");
}
if (body && contentType === ContentType.Multipart) {
const data = await convertMultipartForm(body);
contentType = data.headers["Content-Type"];
body = data.body;
}
const params = {
method: method,
uri: req.uri,
body,
contentType,
headers: req.headers,
query: req.query,
};
await convertKVParams(collection, params.query);
await convertKVParams(collection, params.headers);
if (isWebMode()) {
const ms = Math.random() * 2000;
await delay(ms);
const headers = new Map();
headers.set("content-type", ["application/json"]);
headers.set("set-cookie", ["uid=ZHGG9VYP; path=/; httponly"]);
const resp = {
api: id,
req: req,
latency: Math.ceil(ms),
status: 200,
bodySize: 0,
headers,
body: encode(JSON.stringify(params)),
stats: {
isHttps: false,
cipher: "",
remoteAddr: "127.0.0.1:80",
dnsLookup: 1,
tcp: 2,
tls: 3,
send: 0,
serverProcessing: 4,
contentTransfer: 5,
total: 20,
},
};
addLatestResponse(resp);
return Promise.resolve(resp);
}
if (!userAgent) {
userAgent = await getUserAgent();
}
params.headers.push({
key: "User-Agent",
value: userAgent,
enabled: true,
});
const auth = req.auth.filter((item) => item.enabled);
if (auth.length) {
const value = encode(`${auth[0].key}:${auth[0].value}`);
params.headers.push({
key: "Authorization",
value: `Basic ${value}`,
enabled: true,
});
}
const requestTimeout = {
connect: 10,
write: 120,
read: 300,
};
if (timeout.connect && timeout.connect > 0) {
requestTimeout.connect = timeout.connect;
}
if (timeout.write && timeout.write > 0) {
requestTimeout.write = timeout.write;
}
if (timeout.read && timeout.read > 0) {
requestTimeout.read = timeout.read;
}
// eslint-disable-next-line
// @ts-ignore
let resp: HTTPResponse = {};
const startedAt = Date.now();
try {
resp = await run(cmdDoHTTPRequest, {
req: params,
api: id,
timeout: requestTimeout,
});
} catch (err) {
resp.body = formatError(err);
resp.latency = Date.now() - startedAt;
}
if (resp.latency <= 0) {
resp.latency = 1;
}
// 转换为Map
const headers = new Map();
forEach(resp.headers, (value, key) => {
const k = key.toString();
if (isArray(value)) {
headers.set(k, value);
} else {
headers.set(k, [value as string]);
}
});
resp.req = originalReq;
resp.headers = headers;
addLatestResponse(resp);
return resp;
}
================================================
FILE: src/commands/http_response.ts
================================================
import { decode } from "js-base64";
import dayjs from "dayjs";
import { forEach } from "lodash-es";
import mitt, { Emitter } from "mitt";
import { getLatestResponseStore } from "../stores/local";
import { HTTPRequest } from "./http_request";
import { ulid } from "ulid";
const applicationJSON = "application/json";
export interface HTTPStats {
remoteAddr: string;
isHttps: boolean;
cipher: string;
dnsLookup: number;
tcp: number;
tls: number;
send: number;
serverProcessing: number;
contentTransfer: number;
total: number;
}
export interface HTTPResponse {
[key: string]: unknown;
// response id
id?: string;
// api id
api: string;
req: HTTPRequest;
// 原始body的大小(未解压)
bodySize: number;
// 耗时(ms)
latency: number;
status: number;
headers: Map;
body: string;
stats: HTTPStats;
}
const selectEvent = "select";
type Events = {
[selectEvent]: HTTPResponse;
};
const emitter: Emitter = mitt();
export enum ResponseBodyCategory {
JSON = "json",
Binary = "binary",
Text = "text",
}
export interface ResponseBodyResult {
category: ResponseBodyCategory;
data: string;
size: number;
json?: Map;
}
const statusTextMap = new Map();
(() => {
const dict = {
100: "Continue",
101: "Switching Protocols",
102: "Processing",
103: "Early Hints",
200: "OK",
201: "Created",
202: "Accepted",
203: "Non-Authoritative Information",
204: "No Content",
205: "Reset Content",
206: "Partial Content",
207: "Multi-Status",
208: "Already Reported",
226: "IM Used",
300: "Multiple Choices",
301: "Moved Permanently",
302: "Found",
303: "See Other",
304: "Not Modified",
305: "Use Proxy",
307: "Temporary Redirect",
308: "Permanent Redirect",
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Timeout",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Precondition Failed",
413: "Request Entity Too Large",
414: "Request URI Too Long",
415: "Unsupported Media Type",
416: "Requested Range Not Satisfiable",
417: "Expectation Failed",
418: "I'm a teapot",
421: "Misdirected Request",
422: "Unprocessable Entity",
423: "Locked",
424: "Failed Dependency",
425: "Too Early",
426: "Upgrade Required",
428: "Precondition Required",
429: "Too Many Requests",
431: "Request Header Fields Too Large",
451: "Unavailable For Legal Reasons",
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Timeout",
505: "HTTP Version Not Supported",
506: "Variant Also Negotiates",
507: "Insufficient Storage",
508: "Loop Detected",
510: "Not Extended",
511: "Network Authentication Required",
};
forEach(dict, (value, key) => {
statusTextMap.set(key.toString(), value);
});
})();
export function getStatusText(code: number) {
return statusTextMap.get(code.toString()) || "";
}
export function getResponseBody(resp: HTTPResponse): ResponseBodyResult {
const { headers, body } = resp;
let category = ResponseBodyCategory.Binary;
let data = body;
let size = -1;
let json: Map = new Map();
let isJSON = false;
headers.forEach((values, key) => {
const k = key.toLowerCase();
switch (k) {
case "content-type":
{
const mimeTextReg = /text|javascript/gi;
const value = values.join(" ");
if (value.includes(applicationJSON)) {
category = ResponseBodyCategory.JSON;
data = decode(data);
json = JSON.parse(data);
isJSON = true;
// format
data = JSON.stringify(json, null, 4);
} else if (mimeTextReg.test(value)) {
category = ResponseBodyCategory.Text;
data = decode(data);
}
}
break;
case "content-length":
{
const v = Number.parseInt(values[0]);
if (!Number.isNaN(v)) {
size = v;
}
}
break;
}
});
if (size < 0) {
size = Math.ceil((body.length / 4) * 3);
}
const result: ResponseBodyResult = {
category,
data,
size,
};
if (isJSON) {
result.json = json;
}
return result;
}
// 缓存的response数据
export interface Response {
resp: HTTPResponse;
createdAt: string;
}
// 每个响应保存的记录限制数
const limit = 10;
export async function addLatestResponse(resp: HTTPResponse) {
resp.id = ulid();
const id = resp.api;
const store = getLatestResponseStore();
if (!id || !store) {
return;
}
const arr = (await store.getItem(id)) || [];
if (arr.length >= limit) {
arr.pop();
}
// 添加至顶部
arr.unshift({
resp,
createdAt: dayjs().format(),
});
await store.setItem(id, arr);
}
export async function getLatestResponseList(id: string) {
const arr = (await getLatestResponseStore().getItem(id)) || [];
return arr;
}
export async function clearLatestResponseList(id: string) {
const store = getLatestResponseStore();
if (!id || !store) {
return;
}
await store.setItem(id, []);
}
export async function getLatestResponse(id: string) {
const arr = await getLatestResponseList(id);
if (arr && arr.length) {
return arr[0].resp;
}
}
export function onSelectResponse(ln: (resp: HTTPResponse) => void) {
const fn = (resp: HTTPResponse) => {
ln(resp);
};
emitter.on(selectEvent, fn);
return () => {
emitter.off(selectEvent, fn);
};
}
export function selectResponse(resp: HTTPResponse) {
emitter.emit(selectEvent, resp);
}
================================================
FILE: src/commands/import_api.ts
================================================
import { Promise } from "bluebird";
import { get, uniq, forEach, has } from "lodash-es";
import dayjs from "dayjs";
import { ulid } from "ulid";
import { SettingType } from "../stores/api_setting";
import { APIFolder, createAPIFolder, newDefaultAPIFolder } from "./api_folder";
import {
APISetting,
createAPISetting,
newDefaultAPISetting,
} from "./api_setting";
import { ContentType, HTTPRequest } from "./http_request";
import { KVParam } from "./interface";
import {
createVariable,
newDefaultVariable,
Variable,
VariableCategory,
} from "./variable";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import parseCurl from "../helpers/curl";
import { i18nCommon } from "../i18n";
interface PostManSetting {
name: string;
item?: PostManSetting[];
request?: {
method: string;
url: {
raw: string;
};
query: {
key: string;
value: string;
}[];
body: {
mode: string;
raw: string;
};
};
}
interface InsomniaSetting {
parentId: string;
url: string;
name: string;
method: string;
sort: number;
data: Map;
body: {
mimeType: string;
text: string;
};
headers: {
name: string;
value: string;
}[];
parameters: {
name: string;
value: string;
}[];
_id: string;
_type: string;
}
interface ImportData {
settings: APISetting[];
folders: APIFolder[];
}
export enum ImportCategory {
PostMan = "postMan",
Insomnia = "insomnia",
Swagger = "swagger",
File = "file",
Text = "text",
}
function convertPostManAPISetting(item: PostManSetting, collection: string) {
if (!item.request) {
return;
}
const setting = newDefaultAPISetting();
setting.category = SettingType.HTTP;
setting.collection = collection;
setting.name = item.name;
let contentType = "";
const body = item.request.body?.raw;
if (body && body.startsWith("{") && body.endsWith("}")) {
contentType = ContentType.JSON;
}
const query: KVParam[] = [];
item.request.query?.forEach((q) => {
query.push({
key: q.key,
value: q.value,
enabled: true,
});
});
// TODO headers的处理
let uri = item.request.url?.raw || "";
if (uri && uri.includes("?")) {
const arr = uri.split("?");
uri = arr[0];
// 去除前面host+path部分
arr.shift();
const url = new URL(`http://localhost/?${arr.join("?")}`);
url.searchParams.forEach((value, key) => {
query.push({
key,
value,
enabled: true,
});
});
}
const req: HTTPRequest = {
headers: [],
method: item.request.method,
uri,
contentType,
query,
body: body,
auth: [],
};
setting.setting = JSON.stringify(req);
return setting;
}
function convertSwaggerSetting(params: {
result: ImportData;
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
json: any;
collection: string;
environments: Variable[];
}) {
const { result, json, collection, environments } = params;
const name = get(json, "info.title") || "basePath";
const basePathENV = newDefaultVariable();
basePathENV.category = VariableCategory.Environment;
basePathENV.name = name.replace(/ /g, "");
basePathENV.value = `${get(json, "schemes.0")}://${get(json, "host")}${get(
json,
"basePath",
)}`;
environments.push(basePathENV);
const folderDict = new Map();
forEach(json.paths, (value, uri) => {
forEach(value, (data, method) => {
const setting = newDefaultAPISetting();
setting.collection = collection;
setting.category = SettingType.HTTP;
setting.name = get(data, "summary") || get(data, "operationId");
const query: KVParam[] = [];
const headers: KVParam[] = [];
let contentType = "";
let body = "";
forEach(get(data, "parameters"), (param) => {
if (param.in === "query") {
query.push({
key: param.name,
value: get(param, "example") || "",
enabled: true,
});
} else if (param.in === "body") {
contentType = ContentType.JSON;
const defineKey: string = param.schema?.$ref?.substring(2);
const bodyData: Record = {};
forEach(
get(json, defineKey.replace(/\//g, ".") + ".properties"),
(value, key) => {
const v = value.example;
if (value.type === "boolean") {
bodyData[key] = v === "true";
} else if (value.type !== "string") {
bodyData[key] = Number(v || 0);
} else {
bodyData[key] = v || "";
}
},
);
body = JSON.stringify(bodyData, null, 4);
}
});
const req: HTTPRequest = {
headers,
method: method.toUpperCase(),
uri: `{{${basePathENV.name}}}${uri}`,
contentType,
query,
body,
auth: [],
};
setting.setting = JSON.stringify(req);
result.settings.push(setting);
const tag = get(data, "tags.0");
if (tag) {
let folder = folderDict.get(tag);
if (!folder) {
folder = newDefaultAPIFolder();
folder.collection = collection;
folder.name = tag;
folderDict.set(tag, folder);
result.folders.push(folder);
}
const children = folder.children.split(",");
children.push(setting.id);
folder.children = children.join(",");
}
});
});
}
function convertPostManSetting(params: {
result: ImportData;
items: PostManSetting[];
collection: string;
parentChildren: string[];
}) {
const { result, items, collection, parentChildren } = params;
if (!items || items.length === 0) {
return;
}
items.forEach((item) => {
// api 接口
if (item.request) {
const setting = convertPostManAPISetting(item, collection);
if (setting) {
result.settings.push(setting);
parentChildren.push(setting.id);
}
} else {
// folder
const folder = newDefaultAPIFolder();
result.folders.push(folder);
parentChildren.push(folder.id);
folder.collection = collection;
folder.name = item.name;
const subChildren: string[] = [];
convertPostManSetting({
result,
items: item.item || [],
collection,
parentChildren: subChildren,
});
folder.children = subChildren.join(",");
}
});
}
function convertInsomniaSetting(params: {
result: ImportData;
items: InsomniaSetting[];
collection: string;
}) {
const { result, collection, items } = params;
const subChildrenMap: Map = new Map();
const parentIDMap: Map = new Map();
const addToParent = (id: string, parentID: string) => {
const pid = parentIDMap.get(parentID);
if (pid) {
const arr = subChildrenMap.get(pid) || [];
arr.push(id);
subChildrenMap.set(pid, arr);
}
};
items.forEach((item) => {
if (item._type === "request_group") {
// folder
const folder = newDefaultAPIFolder();
result.folders.push(folder);
folder.collection = collection;
folder.name = item.name;
subChildrenMap.set(folder.id, []);
parentIDMap.set(item._id, folder.id);
addToParent(folder.id, item.parentId);
return;
}
const setting = newDefaultAPISetting();
setting.category = SettingType.HTTP;
setting.collection = collection;
setting.name = item.name;
const body = item.body.text;
let contentType = "";
if (body && body.startsWith("{") && body.endsWith("}")) {
contentType = ContentType.JSON;
}
const query: KVParam[] = [];
item.parameters?.forEach((q) => {
if (!q.value && !q.name) {
return;
}
query.push({
key: q.name,
value: q.value,
enabled: true,
});
});
const headers: KVParam[] = [];
item.headers?.forEach((q) => {
if (!q.value && !q.name) {
return;
}
headers.push({
key: q.name,
value: q.value,
enabled: true,
});
});
let uri = item.url || "";
if (uri && uri.includes("?")) {
const arr = uri.split("?");
uri = arr[0];
// 去除前面host+path部分
arr.shift();
const url = new URL(`http://localhost/?${arr.join("?")}`);
url.searchParams.forEach((value, key) => {
query.push({
key,
value,
enabled: true,
});
});
}
const req: HTTPRequest = {
headers: headers,
method: item.method,
uri,
contentType,
query,
body: body,
auth: [],
};
setting.setting = JSON.stringify(req);
addToParent(setting.id, item.parentId);
result.settings.push(setting);
});
result.folders.forEach((folder) => {
const arr = subChildrenMap.get(folder.id);
if (arr) {
folder.children = arr.join(",");
}
});
}
export async function importAPI(params: {
category: ImportCategory;
collection: string;
fileData: string;
}): Promise {
let category = params.category;
const { collection } = params;
const result: ImportData = {
settings: [],
folders: [],
};
if (params.fileData.startsWith("curl")) {
const req = parseCurl(params.fileData);
const id = ulid();
await createAPISetting({
category: SettingType.HTTP,
collection: params.collection,
name: i18nCommon("untitled"),
id,
setting: JSON.stringify(req),
createdAt: dayjs().format(),
updatedAt: dayjs().format(),
});
return [id];
}
const json = JSON.parse(params.fileData);
const environments: Variable[] = [];
if (has(json, "swagger")) {
category = ImportCategory.Swagger;
} else if (has(json, "item")) {
category = ImportCategory.PostMan;
} else if (has(json, "resources")) {
category = ImportCategory.Insomnia;
}
switch (category) {
case ImportCategory.Swagger: {
convertSwaggerSetting({
result,
json,
collection,
environments,
});
break;
}
case ImportCategory.PostMan:
{
if (!Array.isArray(json.item)) {
return [];
}
const arr = json.item as PostManSetting[];
convertPostManSetting({
result,
items: arr,
collection,
parentChildren: [],
});
forEach(json.variable as [], (item: { key: string; value: string }) => {
const env = newDefaultVariable();
env.category = VariableCategory.Environment;
env.name = item.key;
env.value = item.value;
environments.push(env);
});
}
break;
case ImportCategory.Insomnia:
{
const items = get(json, "resources");
if (!Array.isArray(items)) {
return [];
}
let arr = items as InsomniaSetting[];
arr.forEach((item) => {
if (item._type === "environment") {
forEach(item.data, (value, key) => {
const env = newDefaultVariable();
env.category = VariableCategory.Environment;
env.name = key;
env.value = value as string;
environments.push(env);
});
return;
}
if (item._type === "request_group") {
if (item.parentId.startsWith("wrk_")) {
item.sort = 0;
} else {
item.sort = 1;
}
} else {
item.sort = 2;
}
});
arr = arr.filter((item) =>
["request", "request_group"].includes(item._type),
);
arr.sort((item1, item2) => {
return item1.sort - item2.sort;
});
convertInsomniaSetting({
result,
items: arr,
collection,
});
}
break;
case ImportCategory.Text:
case ImportCategory.File:
{
const arr = Array.isArray(json) ? json : [json];
arr.forEach((item) => {
item.collection = collection;
if (item.category === SettingType.HTTP) {
result.settings.push(item);
} else {
result.folders.push(item);
}
});
}
break;
default:
throw new Error(`${category} is not supported`);
break;
}
const topIDList: string[] = [];
const childrenIDMap: Map = new Map();
result.folders.forEach((item) => {
const children = item.children?.split(",") || [];
children.forEach((id) => {
childrenIDMap.set(id, true);
});
});
await Promise.each(result.folders, async (item) => {
if (!childrenIDMap.has(item.id)) {
topIDList.push(item.id);
}
await createAPIFolder(item);
});
await Promise.each(result.settings, async (item) => {
if (!childrenIDMap.has(item.id)) {
topIDList.push(item.id);
}
await createAPISetting(item);
});
await Promise.each(environments, async (item) => {
if (!item.name && !item.value) {
return;
}
item.collection = collection;
await createVariable(item);
});
return uniq(topIDList);
}
================================================
FILE: src/commands/interface.ts
================================================
export interface KVParam {
[key: string]: unknown;
key: string;
value: string;
enabled: boolean;
}
================================================
FILE: src/commands/invoke.ts
================================================
import { invoke, InvokeArgs } from "@tauri-apps/api/tauri";
import Debug from "debug";
import { isWebMode } from "../helpers/util";
export const cmdInitTables = "init_tables";
export const cmdExportTables = "export_tables";
export const cmdImportTables = "import_tables";
export const cmdAddAPISetting = "add_api_setting";
export const cmdUpdateAPISetting = "update_api_setting";
export const cmdListAPISetting = "list_api_setting";
export const cmdDeleteAPISettings = "delete_api_settings";
export const cmdAddAPIFolder = "add_api_folder";
export const cmdListAPIFolder = "list_api_folder";
export const cmdUpdateAPIFolder = "update_api_folder";
export const cmdDeleteAPIFolder = "delete_api_folder";
export const cmdAddAPICollection = "add_api_collection";
export const cmdUpdateAPICollection = "update_api_collection";
export const cmdListAPICollection = "list_api_collection";
export const cmdDeleteAPICollection = "delete_api_collection";
export const cmdDoHTTPRequest = "do_http_request";
export const cmdListCookie = "list_cookie";
export const cmdDeleteCookie = "delete_cookie";
export const cmdAddCookie = "add_cookie";
export const cmdClearCookie = "clear_cookie";
export const cmdAddVariable = "add_variable";
export const cmdUpdateVariable = "update_variable";
export const cmdDeleteVariable = "delete_variable";
export const cmdListVariable = "list_variable";
export const cmdGetLatestVersion = "get_latest_version";
export const cmdAddVersion = "add_version";
const debug = Debug("invoke");
export async function run(cmd: string, args?: InvokeArgs): Promise {
if (isWebMode()) {
debug("invoke, cmd:%s, args:%o", cmd, args);
// eslint-disable-next-line
// @ts-ignore: mock
return Promise.resolve(null);
}
try {
const result = await invoke(cmd, args);
debug("invoke, result:%o", result);
return result;
} catch (err) {
// eslint-disable-next-line
// @ts-ignore: mock
const message = `[${err.category}]${err.message}`;
throw new Error(message);
}
}
================================================
FILE: src/commands/variable.ts
================================================
import { ulid } from "ulid";
import dayjs from "dayjs";
import { isWebMode } from "../helpers/util";
import {
cmdAddVariable,
cmdDeleteVariable,
cmdListVariable,
cmdUpdateVariable,
run,
} from "./invoke";
import { fakeList, fakeAdd, fakeUpdate, fakeUpdateStore } from "./fake";
const store = "variables";
export enum VariableStatus {
Enabled = "1",
Disabled = "0",
}
export enum VariableCategory {
// 环境变量
Environment = "env",
// 自定义
Customize = "customize",
// 全局请求头
GlobalReqHeaders = "globalReqHeaders",
}
export interface Variable {
[key: string]: unknown;
id: string;
category: string;
collection: string;
// 名称
name: string;
// 值
value: string;
// 是否启用(0:禁用 1:启用)
enabled: string;
// 创建时间
createdAt: string;
// 更新时间
updatedAt: string;
}
export function newDefaultVariable(): Variable {
const id = ulid();
return {
id,
category: "",
collection: "",
name: "",
value: "",
enabled: VariableStatus.Enabled,
createdAt: dayjs().format(),
updatedAt: dayjs().format(),
};
}
export async function createVariable(value: Variable) {
if (isWebMode()) {
await fakeAdd(store, value);
}
await run(cmdAddVariable, {
value,
});
}
export async function listVariable(
collection: string,
category: string,
): Promise {
if (isWebMode()) {
return await fakeList(store);
}
return await run(cmdListVariable, {
collection,
category,
});
}
export async function updateVariable(value: Variable) {
if (isWebMode()) {
return await fakeUpdate(store, value);
return;
}
await run(cmdUpdateVariable, {
value,
});
}
export async function deleteVariable(ids: string[]) {
if (isWebMode()) {
const arr = await fakeList(store);
const result = arr.filter((item) => {
return !ids.includes(item.id);
});
await fakeUpdateStore(store, result);
}
await run(cmdDeleteVariable, {
ids,
});
}
================================================
FILE: src/commands/window.ts
================================================
import { run } from "./invoke";
import { appWindow, LogicalSize, getAll } from "@tauri-apps/api/window";
import { isWebMode } from "../helpers/util";
export function closeSplashscreen() {
run("close_splashscreen");
}
export function showSplashscreen() {
if (isWebMode()) {
return;
}
getAll().forEach((item) => {
if (item.label === "splashscreen") {
item.show();
}
});
}
export async function setWindowSize(width: number, height: number) {
if (isWebMode()) {
return;
}
// 如果有设置小于0,则最大化
if (width < 0 || height < 0) {
await appWindow.maximize();
} else if (width > 0 && height > 0) {
await appWindow.setSize(new LogicalSize(width, height));
}
}
================================================
FILE: src/components/APIResponse/index.tsx
================================================
import {
defineComponent,
onBeforeUnmount,
onMounted,
PropType,
ref,
StyleValue,
watch,
} from "vue";
import { css } from "@linaria/core";
import prettyBytes from "pretty-bytes";
import {
BowlingBallOutline,
InformationCircleOutline,
LinkOutline,
} from "@vicons/ionicons5";
import { editor } from "monaco-editor/esm/vs/editor/editor.api";
import {
HTTPResponse,
ResponseBodyResult,
getLatestResponse,
getResponseBody,
HTTPStats,
} from "../../commands/http_response";
import { useSettingStore } from "../../stores/setting";
import {
NDescriptions,
NDescriptionsItem,
NDivider,
NIcon,
NPopover,
NSpace,
useMessage,
} from "naive-ui";
import { useRoute } from "vue-router";
import { padding } from "../../constants/style";
import { replaceContent, createEditor } from "../../helpers/editor";
import { i18nCollection, i18nCommon } from "../../i18n";
import { convertRequestToCURL, HTTPRequest } from "../../commands/http_request";
import {
convertHTTPHeaderName,
showError,
writeFileToDownload,
writeTextToClipboard,
formatLatency,
} from "../../helpers/util";
import ExPreview, { isSupportPreview } from "../ExPreview";
import ExTimer from "../ExTimer";
import APIResponseList from "./list";
import APIResponseStatusText from "./status_text";
import { toUint8Array } from "js-base64";
import { useCookieStore } from "../../stores/cookie";
import { useAPISettingStore } from "../../stores/api_setting";
const responseClass = css`
margin-left: 5px;
margin-right: 2px;
.infos {
height: 48px;
line-height: 48px;
padding: 0 ${padding}px;
}
.codeEditor,
.previewWrapper {
position: absolute;
top: 50px;
left: 5px;
right: 2px;
bottom: 0;
overflow: auto;
}
.previewWrapper {
z-index: 99;
background-color: rgb(255, 255, 255);
}
.previewWrapper.isDark {
background-color: rgb(30, 30, 30);
}
.n-divider {
margin: 0;
}
.info {
cursor: pointer;
float: left;
margin-top: 15px;
padding: 0 2px;
font-size: 20px;
font-weight: 600;
}
.cookie {
margin-top: 16px;
}
.header {
padding: 0 5px;
cursor: pointer;
font-size: 16px;
}
.responseList {
float: right;
}
`;
const showCurlLimitSize = 2 * 1024;
export default defineComponent({
name: "APIResponse",
props: {
response: {
type: Object as PropType,
default: () => {
return {
status: -1,
headers: new Map(),
body: "",
};
},
},
},
setup(props) {
const route = useRoute();
const message = useMessage();
const settingStore = useSettingStore();
const cookieStore = useCookieStore();
const apiSettingStore = useAPISettingStore();
let editorIns: editor.IStandaloneCodeEditor | null;
const destroy = () => {
if (editorIns) {
editorIns = null;
}
};
const statusCode = ref(0);
const size = ref(-1);
const originalSize = ref(-1);
const latency = ref(0);
const apiID = ref("");
const stats = ref({} as HTTPStats);
const headers = ref(new Map());
const collection = route.query.collection as string;
const previewMode = ref(false);
const previewData = ref({
contentType: "",
data: "",
});
let req: HTTPRequest;
const reqExists = ref(false);
const curl = ref("");
const fillValues = async (resp: HTTPResponse) => {
// 初始加载时,读取最近的响应
let isFromCache = false;
if (!resp.status) {
const tmp = await getLatestResponse(resp.api);
isFromCache = true;
if (tmp) {
resp = tmp;
}
}
statusCode.value = resp.status;
let body = {
size: -1,
} as ResponseBodyResult;
if (resp.body) {
body = getResponseBody(resp);
}
let contentType = "";
let filename = "";
resp.headers?.forEach((values, key) => {
const k = key.toLowerCase();
switch (k) {
case "content-type":
contentType = values[0];
break;
case "content-disposition":
{
const reg = /filename="([\s\S]*?)"/;
const result = reg.exec(values[0]);
if (result?.length === 2) {
filename = result[1];
}
}
break;
}
});
if (isSupportPreview(contentType)) {
previewMode.value = true;
previewData.value = {
contentType,
data: body.data,
};
editorIns?.setValue("");
} else {
previewMode.value = false;
}
originalSize.value = resp.bodySize;
size.value = body.size;
latency.value = resp.latency;
apiID.value = resp.api;
headers.value = resp.headers;
req = resp.req;
if (!req) {
reqExists.value = false;
} else {
reqExists.value = true;
}
curl.value = "";
stats.value = resp.stats;
if (filename) {
if (!isFromCache) {
writeFileToDownload(filename, toUint8Array(body.data))
.then(() => {
message.info(i18nCommon("saveToDownloadSuccess"));
})
.catch((err) => {
showError(message, err);
});
}
} else if (!previewMode.value) {
replaceContent(editorIns, body.data);
}
};
const handleToCURL = async () => {
if (!req || curl.value) {
return;
}
try {
// 由于cookie会一直更新,因此此时再拉取
await cookieStore.fetch();
// req 对象为未调用uri部分,需要先调整
apiSettingStore.fillValues(req);
const value = await convertRequestToCURL(
collection,
req,
cookieStore.cookies,
);
if (value.length > showCurlLimitSize) {
await writeTextToClipboard(value);
}
curl.value = value;
} catch (err) {
showError(message, err);
}
};
const stop = watch(
() => props.response,
(resp) => {
fillValues(resp);
editorIns?.setScrollTop(0);
},
);
const codeEditor = ref();
const initEditor = () => {
if (codeEditor.value) {
editorIns = createEditor({
readonly: true,
dom: codeEditor.value,
isDark: settingStore.isDark,
});
}
};
onMounted(() => {
initEditor();
if (props.response.api) {
fillValues(props.response);
}
});
onBeforeUnmount(() => {
destroy();
stop();
});
return {
curl,
reqExists,
headers,
size,
originalSize,
stats,
latency,
statusCode,
apiID,
previewMode,
previewData,
codeEditor,
handleToCURL,
isDark: settingStore.isDark,
};
},
render() {
const {
statusCode,
size,
originalSize,
latency,
apiID,
curl,
headers,
reqExists,
stats,
previewMode,
previewData,
} = this;
let statusCodeInfo = ;
if (statusCode === -1) {
statusCodeInfo = (
{i18nCollection("requesting")}
);
} else if (statusCode) {
statusCodeInfo = ;
}
const apiIDSlots = {
trigger: () => (
),
};
const headerSlots = {
trigger: () => ,
};
const curlSlots = {
trigger: () => (
),
};
const cookieSlots = {
trigger: () => (
),
};
const isTooLarge = curl.length > showCurlLimitSize;
let curlText = i18nCollection("curlTooLargeTips");
if (!isTooLarge) {
curlText = curl;
}
if (curl.length === 0) {
curlText = i18nCollection("curlGenerateFail");
}
const curlStyle: StyleValue = isTooLarge
? {}
: {
width: "400px",
"word-break": "break-all",
"word-wrap": "break-word",
};
const descriptionItemOptions = [
{
label: i18nCollection("apiID"),
key: "apiID",
value: apiID,
},
];
if (stats?.remoteAddr) {
descriptionItemOptions.push({
label: i18nCollection("remoteAddr"),
key: "remoteAddr",
value: stats.remoteAddr,
});
}
if (stats?.cipher) {
descriptionItemOptions.push({
label: i18nCollection("cipher"),
key: "cipher",
value: `${stats.cipher}`,
});
}
if (stats?.dnsLookup) {
descriptionItemOptions.push({
label: i18nCollection("dns"),
key: "dns",
value: formatLatency(stats.dnsLookup),
});
}
if (stats) {
descriptionItemOptions.push({
label: i18nCollection("tcp"),
key: "tcp",
value: formatLatency(stats.tcp),
});
if (stats.isHttps) {
descriptionItemOptions.push({
label: i18nCollection("tls"),
key: "tls",
value: formatLatency(stats.tls),
});
}
descriptionItemOptions.push(
{
label: i18nCollection("send"),
key: "send",
value: formatLatency(stats.send),
},
{
label: i18nCollection("serverProcessing"),
key: "serverProcessing",
value: formatLatency(stats.serverProcessing),
},
{
label: i18nCollection("contentTransfer"),
key: "contentTransfer",
value: formatLatency(stats.contentTransfer),
},
);
}
const headerDescriptionItems: JSX.Element[] = [];
const cookieDescriptionItems: JSX.Element[] = [];
const setCookieKey = "set-cookie";
if (headers && headers.size !== 0) {
headers.forEach((values, key) => {
values.forEach((value, index) => {
if (key === setCookieKey) {
const cookie = value.split(";")[0];
if (!cookie) {
return;
}
const arr = cookie.split("=");
cookieDescriptionItems.push(
{arr.slice(1).join("=")}
,
);
return;
}
headerDescriptionItems.push(
{value}
,
);
});
});
}
const descriptionItems = descriptionItemOptions.map((item) => {
return (
{item.value}
);
});
const codeEditorCls = {
hidden: false,
};
if (previewMode) {
codeEditorCls.hidden = true;
}
const previewWrapperCls = {
previewWrapper: true,
isDark: false,
};
if (this.isDark) {
previewWrapperCls.isDark = true;
}
const popupContentStyle: StyleValue = {
maxWidth: "600px",
wordBreak: "break-all",
wordWrap: "break-word",
};
const sizeDesc: string[] = [];
if (size > 0) {
sizeDesc.push(prettyBytes(size));
if (originalSize > 0 && Math.abs(size - originalSize) > 100) {
const percent = `${Math.ceil((originalSize * 100) / size)}%`;
sizeDesc.push(`${prettyBytes(originalSize)}(${percent})`);
}
}
return (
{apiID && (
)}
{apiID && (
{descriptionItems}
)}
{cookieDescriptionItems.length !== 0 && (
{cookieDescriptionItems}
)}
{headerDescriptionItems.length !== 0 && (
{headerDescriptionItems}
)}
{reqExists && (
{
if (value) {
this.handleToCURL();
}
}}
>
{curlText}
)}
{statusCodeInfo}
{/* 占位 */}
{sizeDesc.length !== 0 && sizeDesc.join(" / ")}
{/* 占位 */}
{latency > 0 && formatLatency(latency)}
{codeEditorCls.hidden && (
)}
);
},
});
================================================
FILE: src/components/APIResponse/list.tsx
================================================
import {
NButton,
NDivider,
NDropdown,
NIcon,
NP,
NSpace,
useMessage,
} from "naive-ui";
import { css } from "@linaria/core";
import prettyBytes from "pretty-bytes";
import { isNumber } from "lodash-es";
import { defineComponent, ref } from "vue";
import {
clearLatestResponseList,
getLatestResponseList,
Response,
selectResponse,
} from "../../commands/http_response";
import { formatSimpleDate, showError } from "../../helpers/util";
import { ListOutline, TrashOutline } from "@vicons/ionicons5";
import { DropdownMixedOption } from "naive-ui/es/dropdown/src/interface";
import { i18nStore } from "../../i18n";
const showMoreClass = css`
cursor: pointer;
margin: 15px 10px 0 0;
padding: 0 5px;
`;
export default defineComponent({
name: "APIResponseList",
props: {
id: {
type: String,
required: true,
},
},
setup(props) {
const message = useMessage();
const responseList = ref([] as Response[]);
const handleFetch = async () => {
responseList.value.length = 0;
try {
const arr = await getLatestResponseList(props.id);
responseList.value = arr;
} catch (err) {
showError(message, err);
}
};
const handleSelect = (index: number) => {
const resp = responseList.value[index];
if (resp) {
selectResponse(resp.resp);
}
};
const handleClearHistory = async () => {
try {
await clearLatestResponseList(props.id);
message.info(i18nStore("clearHistorySuccess"));
responseList.value.length = 0;
} catch (err) {
showError(message, err);
}
};
return {
responseList,
handleFetch,
handleSelect,
handleClearHistory,
};
},
render() {
const { responseList } = this;
const options: DropdownMixedOption[] = responseList.map((item, index) => {
let bodySize = "--";
if (item.resp && isNumber(item.resp.bodySize)) {
bodySize = prettyBytes(item.resp.bodySize);
}
return {
label: () => (
{item.resp.status}
{bodySize}
{formatSimpleDate(item.createdAt)}
),
key: index,
};
});
const clearHistorySlots = {
icon: () => (
),
};
const clearBtn = responseList?.length !== 0 && (
{
this.handleClearHistory();
}}
>
{i18nStore("clearHistory")}
);
const tips = responseList?.length === 0 && (
{i18nStore("noHistory")}
);
options.unshift({
key: "header",
type: "render",
render: () => (
{clearBtn}
{i18nStore("responseList")}
{tips}
),
});
return (
{
this.handleSelect(value);
}}
onUpdateShow={(value) => {
if (value) {
this.handleFetch();
}
}}
options={options}
showArrow={true}
>
);
},
});
================================================
FILE: src/components/APIResponse/status_text.tsx
================================================
import { NGradientText } from "naive-ui";
import { defineComponent } from "vue";
import { getStatusText } from "../../commands/http_response";
function getStatusType(statusCode: number) {
if (statusCode >= 500) {
return "error";
}
if (statusCode >= 400) {
return "warning";
}
return "success";
}
export default defineComponent({
name: "APIResponseStatusText",
props: {
statusCode: {
type: Number,
required: true,
},
},
render() {
const { statusCode } = this.$props;
return (
{statusCode} {getStatusText(statusCode)}
);
},
});
================================================
FILE: src/components/APISettingParams/index.tsx
================================================
import {
defineComponent,
watch,
ref,
onBeforeUnmount,
PropType,
VNode,
} from "vue";
import { css } from "@linaria/core";
import { NDivider, useMessage } from "naive-ui";
import { storeToRefs } from "pinia";
import { cloneDeep, debounce } from "lodash-es";
import { useAPISettingStore } from "../../stores/api_setting";
import { abortRequestID, HTTPRequest } from "../../commands/http_request";
import { showError } from "../../helpers/util";
import { i18nCollection } from "../../i18n";
import APISettingParamsURI, { RequestURI } from "./uri";
import APISettingParamsReqParams from "./req_params";
import { KVParam } from "../../commands/interface";
import { onSelectResponse } from "../../commands/http_response";
const wrapperClass = css`
height: 100%;
position: relative;
margin-left: 5px;
.n-divider {
margin: 0;
}
`;
export default defineComponent({
name: "APISettingParams",
props: {
onSend: {
type: Function as PropType<(id: string) => Promise>,
required: true,
},
},
setup() {
const message = useMessage();
const apiSettingStore = useAPISettingStore();
const { selectedID } = storeToRefs(apiSettingStore);
const componentKey = ref(selectedID.value);
const reqParams = ref({} as HTTPRequest);
const reqParamsStyle = ref({
height: "0px",
});
const wrapper = ref();
let uriNodeHeight = 0;
const caclWrapperHeight = () => {
const height = wrapper.value?.clientHeight || 0;
if (!height) {
return;
}
reqParamsStyle.value.height = `${height - uriNodeHeight}px`;
};
const updateURINodeHeight = (node: VNode) => {
uriNodeHeight = node.el?.clientHeight || 0;
caclWrapperHeight();
};
const updateReqParams = (id: string) => {
try {
if (id) {
reqParams.value = apiSettingStore.getHTTPRequest(id);
} else {
reqParams.value = {} as HTTPRequest;
}
} catch (err) {
console.error(err);
} finally {
caclWrapperHeight();
}
};
const stop = watch(selectedID, (id) => {
componentKey.value = id;
updateReqParams(id);
});
if (selectedID.value) {
updateReqParams(selectedID.value);
}
const offListen = onSelectResponse((resp) => {
reqParams.value = cloneDeep(resp.req);
caclWrapperHeight();
const id = resp.id || `${Date.now()}`;
componentKey.value = `${selectedID.value}-${id}`;
});
onBeforeUnmount(() => {
offListen();
stop();
});
const update = async () => {
const id = selectedID.value;
if (!id) {
message.warning(i18nCollection("shouldSelectAPISettingFirst"));
return;
}
const data = apiSettingStore.findByID(id);
if (!data) {
return;
}
try {
let value = "";
if (reqParams.value) {
value = JSON.stringify(reqParams.value);
}
data.setting = value;
await apiSettingStore.updateByID(id, data);
} catch (err) {
showError(message, err);
}
};
const handleUpdateURI = async (data: RequestURI) => {
Object.assign(reqParams.value, data);
await update();
};
const handleUpdateBody = async (
id: string,
params: {
body: string;
contentType: string;
},
) => {
// 因为是延时执行,如果已经切换,则不更新
// 避免更新了其它接口的数据
if (id !== selectedID.value) {
return;
}
reqParams.value.contentType = params.contentType;
reqParams.value.body = params.body;
await update();
};
const newHandleUpdate = (key: string) => {
return async (id: string, data: KVParam[]) => {
// 因为是延时执行,如果已经切换,则不更新
// 避免更新了其它接口的数据
if (id !== selectedID.value) {
return;
}
reqParams.value[key] = data;
await update();
};
};
const handleUpdateAuth = debounce(newHandleUpdate("auth"), 300);
const handleUpdateQuery = debounce(newHandleUpdate("query"), 300);
const handleUpdateHeaders = debounce(newHandleUpdate("headers"), 300);
return {
componentKey,
reqParamsStyle,
updateURINodeHeight,
wrapper,
selectedID,
reqParams,
// 避免频繁重复触发,不能设置过长
// 如果设置过长容易导致更新了还没生效
handleUpdateAuth,
handleUpdateBody: debounce(handleUpdateBody, 300),
handleUpdateURI,
handleUpdateQuery,
handleUpdateHeaders,
};
},
render() {
const { reqParams, selectedID, componentKey } = this;
return (
{
this.updateURINodeHeight(node);
}}
onVnodeUpdated={(node) => {
this.updateURINodeHeight(node);
}}
params={reqParams}
onUpdateURI={(data) => {
this.handleUpdateURI(data);
}}
onSubmit={(isAborted: boolean) => {
if (isAborted) {
return this.$props.onSend(abortRequestID);
}
return this.$props.onSend(selectedID);
}}
/>
{
this.handleUpdateBody(selectedID, value);
}}
onUpdateAuth={(value) => {
this.handleUpdateAuth(selectedID, value);
}}
onUpdateQuery={(value) => {
this.handleUpdateQuery(selectedID, value);
}}
onUpdateHeaders={(value) => {
this.handleUpdateHeaders(selectedID, value);
}}
/>
);
},
});
================================================
FILE: src/components/APISettingParams/req_params.tsx
================================================
import {
NBadge,
NButton,
NButtonGroup,
NDropdown,
NIcon,
NTab,
NTabs,
useDialog,
useMessage,
} from "naive-ui";
import { css } from "@linaria/core";
import {
defineComponent,
onBeforeUnmount,
onMounted,
PropType,
ref,
watch,
} from "vue";
import { editor } from "monaco-editor/esm/vs/editor/editor.api";
import {
HTTPMethod,
HTTPRequest,
ContentType,
} from "../../commands/http_request";
import { useSettingStore } from "../../stores/setting";
import { i18nCollection, i18nCommon } from "../../i18n";
import { CaretDownOutline, CodeSlashOutline } from "@vicons/ionicons5";
import { showError, tryToParseArray } from "../../helpers/util";
import ExKeyValue, { HandleOption } from "../ExKeyValue";
import { KVParam } from "../../commands/interface";
import { padding } from "../../constants/style";
import { useAPICollectionStore } from "../../stores/api_collection";
import { replaceContent, createEditor } from "../../helpers/editor";
enum TabItem {
Body = "Body",
Query = "Query",
Auth = "Auth",
Header = "Header",
}
const tabClass = css`
position: relative;
.expandSelect {
visibility: hidden;
}
.n-tabs:hover .expandSelect {
visibility: visible;
}
.n-tabs-tab__label {
.n-icon {
margin-left: 5px;
}
.contentType {
width: 60px;
text-align: center;
}
}
.badgeTab {
position: relative;
.badge {
position: absolute;
right: -15px;
top: 8px;
.n-badge-sup {
padding: 0 3px !important;
border-radius: 3px !important;
}
}
}
.hidden {
display: none;
}
.format {
position: fixed;
bottom: 2px;
.n-icon {
font-size: 16px;
font-weight: 900;
margin-right: 5px;
}
}
.content {
position: absolute;
top: 40px;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
}
.keyValue {
margin: ${padding}px;
}
`;
function shouldHaveBody(method: string) {
return [HTTPMethod.POST, HTTPMethod.PUT, HTTPMethod.PATCH].includes(
method as HTTPMethod,
);
}
function shouldShowEditor(contentType: string) {
return [ContentType.JSON, ContentType.XML, ContentType.Plain].includes(
contentType as ContentType,
);
}
function createBadgeTab(params: {
tab: string;
value: number;
activeTab: string;
}) {
const { value, tab, activeTab } = params;
const badge =
value && tab !== activeTab ? (
) : null;
return (
{tab}
{badge}
);
}
function createBodyBadge(params: { contentType: string; body: string }) {
const { contentType, body } = params;
if (
![ContentType.Multipart, ContentType.Form].includes(
contentType as ContentType,
)
) {
return;
}
const arr = tryToParseArray(body);
if (arr.length === 0) {
return;
}
return ;
}
export default defineComponent({
name: "APISettingParamsReqParams",
props: {
id: {
type: String,
default: () => "",
},
params: {
type: Object as PropType,
required: true,
},
onUpdateBody: {
type: Function as PropType<
(params: { body: string; contentType: string }) => void
>,
required: true,
},
onUpdateQuery: {
type: Function as PropType<(query: KVParam[]) => void>,
required: true,
},
onUpdateHeaders: {
type: Function as PropType<(headers: KVParam[]) => void>,
required: true,
},
onUpdateAuth: {
type: Function as PropType<(auth: KVParam[]) => void>,
required: true,
},
},
setup(props) {
const settingStore = useSettingStore();
const message = useMessage();
const dialog = useDialog();
const collectionStore = useAPICollectionStore();
const codeEditor = ref();
const contentType = ref(props.params.contentType || ContentType.JSON);
let tab = collectionStore.getActiveTab(props.id);
if (!tab) {
if (shouldHaveBody(props.params.method)) {
tab = TabItem.Body;
} else {
tab = TabItem.Query;
}
}
const activeTab = ref(tab as TabItem);
let editorIns: editor.IStandaloneCodeEditor | null;
const destroy = () => {
if (editorIns) {
editorIns = null;
}
};
const handleEditorUpdate = () => {
if (props.onUpdateBody && editorIns) {
props.onUpdateBody({
body: editorIns.getValue().trim(),
contentType: contentType.value,
});
}
};
const initEditor = () => {
if (editorIns) {
editorIns.setValue(props.params.body);
return;
}
if (codeEditor.value) {
editorIns = createEditor({
dom: codeEditor.value,
isDark: settingStore.isDark,
});
editorIns.setValue(props.params.body || "");
editorIns.onDidChangeModelContent(handleEditorUpdate);
}
};
const handleChangeContentType = (newContentType: string) => {
// 如果无数据,直接切换
const changeContentType = () => {
// 清空
replaceContent(editorIns, "");
if (props.onUpdateBody) {
props.onUpdateBody({
body: "",
contentType: newContentType,
});
}
contentType.value = newContentType;
};
if (!props.params.body) {
changeContentType();
return;
}
dialog.warning({
title: i18nCollection("changeContentType"),
content: i18nCollection("changeContentTypeContent"),
positiveText: i18nCommon("confirm"),
onPositiveClick: async () => {
changeContentType();
},
});
};
const getParamsFromHandleOption = (opt: HandleOption) => {
const arr = [] as KVParam[];
opt.params.forEach((item) => {
const { key, value } = item;
if (!key && !value) {
return;
}
arr.push({
key,
value,
enabled: item.enabled,
});
});
return arr;
};
const handleBodyParams = (opt: HandleOption) => {
const arr = getParamsFromHandleOption(opt);
if (props.onUpdateBody) {
props.onUpdateBody({
body: JSON.stringify(arr),
contentType: contentType.value,
});
}
};
const handleQueryParams = (opt: HandleOption) => {
const arr = getParamsFromHandleOption(opt);
if (props.onUpdateQuery) {
props.onUpdateQuery(arr);
}
};
const handleHeaders = (opt: HandleOption) => {
const arr = getParamsFromHandleOption(opt);
if (props.onUpdateHeaders) {
props.onUpdateHeaders(arr);
}
};
const handleAuth = (opt: HandleOption) => {
const arr = getParamsFromHandleOption(opt);
if (props.onUpdateAuth) {
props.onUpdateAuth(arr);
}
};
const updateParamsColumnWidth = (width: number) => {
settingStore.updateParamsColumnWidth(width);
};
// method变化时要选定对应的tab
const stop = watch(
() => props.params.method,
(method) => {
if (shouldHaveBody(method)) {
activeTab.value = TabItem.Body;
} else {
activeTab.value = TabItem.Query;
}
},
);
const handleUpdateActiveTab = async (activeTab: string) => {
try {
await collectionStore.updateActiveTab({
id: props.id,
activeTab,
});
} catch (err) {
showError(message, err);
}
};
const handleFormat = () => {
if (editorIns) {
editorIns.getAction("editor.action.formatDocument")?.run();
}
};
onMounted(() => {
initEditor();
});
onBeforeUnmount(() => {
stop();
destroy();
});
return {
contentType,
handleBodyParams,
handleQueryParams,
handleHeaders,
handleAuth,
handleChangeContentType,
handleUpdateActiveTab,
handleFormat,
activeTab,
codeEditor,
updateParamsColumnWidth,
};
},
render() {
const { params } = this.$props;
const { method } = params;
const { activeTab, contentType } = this;
const tabs = [TabItem.Query, TabItem.Header, TabItem.Auth];
if (shouldHaveBody(method)) {
tabs.unshift(TabItem.Body);
}
let activeIndex = tabs.indexOf(activeTab);
if (activeIndex < 0) {
activeIndex = 0;
}
const contentTypeOptions = [
{
label: "JSON",
key: ContentType.JSON,
},
{
label: "Form",
key: ContentType.Form,
},
{
label: "Multipart",
key: ContentType.Multipart,
},
{
label: "XML",
key: ContentType.XML,
},
{
label: "Plain",
key: ContentType.Plain,
},
];
const list = tabs.map((item) => {
switch (item) {
case TabItem.Body:
{
const label = contentTypeOptions.find(
(opt) => opt.key === contentType,
);
if (activeTab !== TabItem.Body) {
const badge = createBodyBadge({
contentType,
body: params.body,
});
return (
{label?.label}
{badge}
);
}
return (
{
this.handleChangeContentType(value);
}}
>
{label?.label}
);
}
break;
case TabItem.Query:
return createBadgeTab({
activeTab,
tab: item,
value: params.query?.length,
});
break;
case TabItem.Header:
{
return createBadgeTab({
activeTab,
tab: item,
value: params.headers?.length,
});
}
break;
case TabItem.Auth:
{
return createBadgeTab({
activeTab,
tab: item,
value: params.auth?.length,
});
}
break;
default:
return {item};
break;
}
});
let codeEditorClass = "";
if (activeTab !== TabItem.Body || !shouldShowEditor(contentType)) {
codeEditorClass = "hidden";
}
let showBodyKeyValue = false;
let keyValues = [];
switch (activeTab) {
case TabItem.Body:
{
if (!shouldShowEditor(contentType)) {
showBodyKeyValue = true;
try {
keyValues = tryToParseArray(this.params.body);
} catch (err) {
// 忽略parse出错
console.error(err);
}
}
}
break;
case TabItem.Query:
{
keyValues = this.params.query || [];
}
break;
case TabItem.Header:
{
keyValues = this.params.headers || [];
}
break;
case TabItem.Auth:
{
keyValues = this.params.auth || [];
}
break;
}
const keyValueSpans = [8, 16];
const tabSlots = {
suffix: () => (
{
this.updateParamsColumnWidth(0.3);
}}
>
30%
{
this.updateParamsColumnWidth(0.5);
}}
>
50%
{
this.updateParamsColumnWidth(0.7);
}}
>
70%
),
};
return (
{
let activeTab = value as string;
if (shouldHaveBody(method)) {
if (value === TabItem.Body) {
activeTab = "";
}
} else {
if (value === TabItem.Query) {
activeTab = "";
}
}
this.handleUpdateActiveTab(activeTab);
this.activeTab = value;
}}
>
{list}
{/* json, xml, text */}
{activeTab === TabItem.Body && contentType === ContentType.JSON && (
{
this.handleFormat();
}}
>
{i18nCollection("format")}
)}
{/* body form/multipart */}
{showBodyKeyValue && (
{
this.handleBodyParams(opt);
}}
/>
)}
{activeTab === TabItem.Query && (
{
this.handleQueryParams(opt);
}}
/>
)}
{activeTab === TabItem.Header && (
{
this.handleHeaders(opt);
}}
/>
)}
{activeTab === TabItem.Auth && (
{
this.handleAuth(opt);
}}
/>
)}
);
},
});
================================================
FILE: src/components/APISettingParams/uri.tsx
================================================
import { defineComponent, PropType, ref } from "vue";
import { css } from "@linaria/core";
import {
NButton,
NInput,
NInputGroup,
NSelect,
NIcon,
NDropdown,
NGradientText,
} from "naive-ui";
import { ulid } from "ulid";
import { storeToRefs } from "pinia";
import { CodeSlashOutline } from "@vicons/ionicons5";
import { i18nCollection, i18nEnvironment } from "../../i18n";
import { HTTPRequest, HTTPMethod } from "../../commands/http_request";
import { useEnvironmentStore, ENVRegexp } from "../../stores/environment";
import { useDialogStore } from "../../stores/dialog";
import { VariableStatus } from "../../commands/variable";
const environmentSelectWidth = 50;
const wrapperClass = css`
padding: 7px 4px 5px 0;
overflow: hidden;
.environmentSelect {
width: ${environmentSelectWidth}px;
float: left;
.n-icon {
font-size: 16px;
font-weight: 900;
}
}
.url {
margin-left: ${environmentSelectWidth}px;
.method {
width: 120px;
.n-base-selection {
height: 36px;
}
}
.submit {
width: 80px;
height: 36px;
}
}
.n-input,
.n-base-selection-label {
background-color: transparent !important;
line-break: anywhere;
}
`;
const envLabelClass = css`
padding: 0 5px;
span {
margin-left: 10px;
}
.n-icon {
font-weight: 900;
font-size: 16px;
}
`;
interface CuttingURIResult {
env: string;
uri: string;
}
function cuttingURI(uri: string): CuttingURIResult {
const result = {
env: "",
uri,
};
const arr = ENVRegexp.exec(uri);
if (arr?.length === 2) {
result.env = arr[1].trim();
result.uri = uri.substring(arr[0].length);
}
return result;
}
export interface RequestURI {
method: string;
uri: string;
}
const addNewENVKey = ulid();
const clearENVKey = ulid();
export default defineComponent({
name: "APISettingParamsURI",
props: {
params: {
type: Object as PropType,
required: true,
},
onUpdateURI: {
type: Function as PropType<(value: RequestURI) => void>,
required: true,
},
onSubmit: {
type: Function as PropType<(isAborted: boolean) => Promise>,
required: true,
},
},
setup(props) {
const dialogStore = useDialogStore();
const environmentStore = useEnvironmentStore();
const environments = storeToRefs(environmentStore).variables;
const uriResult = cuttingURI(props.params.uri);
const currentURI = ref(uriResult.uri);
const env = ref(uriResult.env);
const method = ref(props.params.method);
const sending = ref(false);
const showEnvironment = () => {
dialogStore.toggleEnvironmentDialog(true);
};
const handleUpdate = () => {
let uri = currentURI.value || "";
if (env.value) {
uri = `{{${env.value}}}${uri}`;
}
const changed =
uri !== props.params.uri || method.value !== props.params.metod;
if (changed && props.onUpdateURI) {
props.onUpdateURI({
method: method.value,
uri,
});
}
};
let currentID = "";
const isCurrent = (id: string) => {
return id === currentID;
};
let lastHandleSendAt = 0;
const handleSend = async () => {
if (!props.onSubmit) {
return;
}
const now = Date.now();
// 如果快速点击
// 直接忽略第二次点击
if (now - lastHandleSendAt < 200) {
return;
}
lastHandleSendAt = now;
// 如果发送中,则中止请求
if (sending.value) {
sending.value = false;
currentID = "";
await props.onSubmit(true);
return;
}
const id = ulid();
currentID = id;
sending.value = true;
try {
await props.onSubmit(false);
} finally {
// 只有当前id才重置状态
if (isCurrent(id)) {
sending.value = false;
}
}
};
return {
sending,
handleSend,
showEnvironment,
handleUpdate,
environments,
method,
env,
currentURI,
};
},
render() {
const { environments, currentURI, env, method } = this;
const options = [
HTTPMethod.GET,
HTTPMethod.POST,
HTTPMethod.PUT,
HTTPMethod.PATCH,
HTTPMethod.DELETE,
HTTPMethod.OPTIONS,
HTTPMethod.HEAD,
].map((item) => {
return {
label: item,
value: item,
};
});
// 只过滤启用的
const envOptions = environments
.filter((item) => item.enabled === VariableStatus.Enabled)
.map((item) => {
return {
label: `${item.name} | ${item.value}`,
key: item.name,
};
});
let envPrefix = "";
if (env) {
envPrefix = env.substring(0, 2).toUpperCase();
}
envOptions.push({
label: i18nEnvironment("addNew"),
key: addNewENVKey,
});
if (this.env) {
envOptions.push({
label: i18nEnvironment("clearCurrent"),
key: clearENVKey,
});
}
const autoSizeOption = { minRows: 1, maxRows: 3 };
return (
{
const label = (option.label as string) || "";
const arr = label.split(" | ");
return (
{arr[0]}
{arr[1] && {arr[1]}}
);
}}
value={env}
onSelect={(value) => {
if (value === addNewENVKey) {
this.showEnvironment();
return;
}
if (value === clearENVKey) {
this.env = "";
} else {
this.env = value;
}
this.handleUpdate();
}}
>
{!envPrefix && (
)}
{envPrefix && {envPrefix}}
{
this.method = value;
this.handleUpdate();
}}
/>
{
this.handleUpdate();
}}
onUpdateValue={(value) => {
this.currentURI = value?.trim();
}}
onKeydown={(e) => {
if (e.key.toLowerCase() === "enter" && this.currentURI) {
this.handleSend();
e.preventDefault();
}
}}
/>
{
this.handleSend();
}}
>
{this.sending ? i18nCollection("abort") : i18nCollection("send")}
);
},
});
================================================
FILE: src/components/APISettingTree/header.tsx
================================================
// API栏目的顶部功能栏
import { defineComponent, inject, onBeforeUnmount, PropType } from "vue";
import { css } from "@linaria/core";
import {
NDropdown,
NButton,
NGi,
NGrid,
NInput,
NIcon,
useMessage,
useDialog,
} from "naive-ui";
import { DropdownMixedOption } from "naive-ui/es/dropdown/src/interface";
import { i18nCollection, i18nCommon } from "../../i18n";
import { SettingType, useAPISettingStore } from "../../stores/api_setting";
import {
AnalyticsOutline,
DownloadOutline,
FolderOpenOutline,
} from "@vicons/ionicons5";
import {
hotKeyCreateFolder,
hotKeyCreateHTTPSetting,
hotKeyMatchCreateFolder,
hotKeyMatchCreateHTTPSetting,
} from "../../helpers/hot_key";
import {
addFolderDefaultValue,
addFolderKey,
addHTTPSettingDefaultValue,
addHTTPSettingKey,
} from "../../constants/provide";
import {
readTextFromClipboard,
showError,
writeSettingToDownload,
} from "../../helpers/util";
import { useRoute } from "vue-router";
import { useAPIFolderStore } from "../../stores/api_folder";
import { useAPICollectionStore } from "../../stores/api_collection";
import { HandleKey } from "../../constants/handle_key";
import { newImportDialog } from "../ExDialog";
const collapseWidth = 50;
const headerClass = css`
margin-right: ${collapseWidth}px;
position: relative;
.collapse {
position: absolute;
top: 0;
right: ${-collapseWidth}px;
bottom: 0;
width: ${collapseWidth}px;
.n-button {
margin-left: 10px;
}
}
`;
const addDropdownClass = css`
.label {
min-width: 180px;
}
.hotKey {
float: right;
}
`;
export default defineComponent({
name: "APISettingTreeHeader",
props: {
onFilter: {
type: Function as PropType<(value: string) => void>,
required: true,
},
},
setup() {
const route = useRoute();
const message = useMessage();
const dialog = useDialog();
const apiFolderStore = useAPIFolderStore();
const apiSettingStore = useAPISettingStore();
const collectionStore = useAPICollectionStore();
const collection = route.query.collection as string;
const addHTTPSetting = inject(
addHTTPSettingKey,
addHTTPSettingDefaultValue,
);
const addFolder = inject(addFolderKey, addFolderDefaultValue);
const handleKeydown = (e: KeyboardEvent) => {
if (hotKeyMatchCreateFolder(e)) {
addFolder("");
return;
}
if (hotKeyMatchCreateHTTPSetting(e)) {
addHTTPSetting("");
return;
}
};
document.addEventListener("keydown", handleKeydown);
onBeforeUnmount(() => {
document.removeEventListener("keydown", handleKeydown);
});
const handleCloseAllFolders = async () => {
try {
await collectionStore.closeAllFolders(collection);
} catch (err) {
showError(message, err);
}
};
const handleImport = async () => {
let data = "";
try {
data = (await readTextFromClipboard()) || "";
} catch (err) {
showError(message, err);
} finally {
newImportDialog({
dialog,
collection,
data,
});
}
};
const handleExport = async () => {
const arr: unknown[] = [];
apiFolderStore.apiFolders.forEach((folder) => arr.push(folder));
apiSettingStore.apiSettings.forEach((apiSetting) => arr.push(apiSetting));
try {
let name = "unknown";
const result = collectionStore.findByID(collection);
if (result) {
name = result.name;
}
await writeSettingToDownload(arr, name);
message.info(i18nCollection("exportSettingsSuccess"));
} catch (err) {
showError(message, err);
}
};
return {
handleImport,
handleExport,
addHTTPSetting,
addFolder,
handleCloseAllFolders,
text: {
add: i18nCommon("add"),
placeholder: i18nCollection("filterPlaceholder"),
},
};
},
render() {
const options: DropdownMixedOption[] = [
{
label: `${i18nCollection(
"newHTTPRequest",
)} | ${hotKeyCreateHTTPSetting()}`,
key: SettingType.HTTP,
icon: () => (
),
},
{
label: `${i18nCollection("newFolder")} | ${hotKeyCreateFolder()}`,
key: SettingType.Folder,
icon: () => (
),
},
{
type: "divider",
key: "divider",
},
{
label: i18nCollection("exportSettings"),
key: HandleKey.ExportSettings,
icon: () => (
),
},
{
label: i18nCollection("importSettings"),
key: HandleKey.ImportSettings,
icon: () => (
),
},
];
const { text } = this;
const inputProps = {
spellcheck: false,
};
return (
);
},
});
================================================
FILE: src/components/APISettingTree/index.tsx
================================================
// API应用配置列表
import { css } from "@linaria/core";
import { defineComponent, provide, ref } from "vue";
import { useDialog, useMessage } from "naive-ui";
import { SettingType, useAPISettingStore } from "../../stores/api_setting";
import APISettingTreeHeader from "./header";
import APISettingTreeItems from "./items";
import ExDialog from "../ExDialog";
import { i18nCollection, i18nCommon } from "../../i18n";
import { ExFormItem } from "../ExForm";
import { newDefaultAPISetting } from "../../commands/api_setting";
import { useRoute } from "vue-router";
import { showError } from "../../helpers/util";
import { useAPIFolderStore } from "../../stores/api_folder";
import { newDefaultAPIFolder } from "../../commands/api_folder";
import { addFolderKey, addHTTPSettingKey } from "../../constants/provide";
const treesClass = css`
padding: 10px;
`;
const getSettingFormItems = (): ExFormItem[] => {
return [
{
key: "name",
label: i18nCommon("name"),
placeholer: i18nCommon("namePlaceholder"),
rule: {
required: true,
message: i18nCommon("nameRequireError"),
trigger: "blur",
},
},
];
};
const getFolderFormItems = (): ExFormItem[] => {
return [
{
key: "name",
label: i18nCommon("name"),
placeholer: i18nCommon("namePlaceholder"),
rule: {
required: true,
message: i18nCommon("nameRequireError"),
trigger: "blur",
},
},
];
};
export default defineComponent({
name: "APISettingTree",
setup() {
const keyword = ref("");
const apiSettingStore = useAPISettingStore();
const apiFolderStore = useAPIFolderStore();
const dialog = useDialog();
const route = useRoute();
const message = useMessage();
const collection = route.query.collection as string;
provide(addHTTPSettingKey, (folder: string) => {
ExDialog({
dialog,
title: i18nCollection("newHTTPRequest"),
formItems: getSettingFormItems(),
onConfirm: async (data) => {
const setting = newDefaultAPISetting();
setting.category = SettingType.HTTP;
setting.collection = collection;
setting.name = data.name as string;
try {
await apiSettingStore.add(setting);
if (folder) {
await apiFolderStore.addChild({
id: folder,
children: [setting.id],
});
}
apiSettingStore.select(setting.id);
} catch (err) {
showError(message, err);
}
},
});
});
provide(addFolderKey, (parentFolder: string) => {
ExDialog({
dialog,
title: i18nCollection("newFolder"),
formItems: getFolderFormItems(),
onConfirm: async (data) => {
const folder = newDefaultAPIFolder();
folder.collection = collection;
folder.name = data.name as string;
try {
await apiFolderStore.add(folder);
if (folder) {
await apiFolderStore.addChild({
id: parentFolder,
children: [folder.id],
});
}
} catch (err) {
showError(message, err);
}
},
});
});
return {
keyword,
};
},
render() {
return (
{
this.keyword = value;
}}
/>
);
},
});
================================================
FILE: src/components/APISettingTree/item_dropdown.tsx
================================================
// API功能下拉选项框
import {
AddOutline,
ChevronDownOutline,
CopyOutline,
CreateOutline,
DownloadOutline,
FolderOpenOutline,
LinkOutline,
PinOutline,
TrashOutline,
} from "@vicons/ionicons5";
import { NDropdown, NIcon, useDialog, useMessage } from "naive-ui";
import { defineComponent, inject } from "vue";
import { useRoute } from "vue-router";
import { css } from "@linaria/core";
import { DropdownMixedOption } from "naive-ui/es/dropdown/src/interface";
import {
readTextFromClipboard,
showError,
writeSettingToDownload,
writeTextToClipboard,
} from "../../helpers/util";
import { i18nCollection, i18nCommon } from "../../i18n";
import { useAPIFolderStore } from "../../stores/api_folder";
import { SettingType, useAPISettingStore } from "../../stores/api_setting";
import { HandleKey } from "../../constants/handle_key";
import ExDialog, { newImportDialog } from "../ExDialog";
import {
addFolderDefaultValue,
addFolderKey,
addHTTPSettingDefaultValue,
addHTTPSettingKey,
} from "../../constants/provide";
import { usePinRequestStore } from "../../stores/pin_request";
import { convertRequestToCURL } from "../../commands/http_request";
import { APISetting } from "../../commands/api_setting";
import { APIFolder } from "../../commands/api_folder";
import { useCookieStore } from "../../stores/cookie";
const dropdownClass = css`
.n-dropdown-option {
margin: 2px 0;
}
`;
export default defineComponent({
name: "APISettingTreeItemDropdown",
props: {
id: {
type: String,
required: true,
},
apiSettingType: {
type: String,
required: true,
},
},
setup(props) {
const dialog = useDialog();
const message = useMessage();
const cookieStore = useCookieStore();
const apiFolderStore = useAPIFolderStore();
const pinRequestStore = usePinRequestStore();
const apiSettingStore = useAPISettingStore();
const route = useRoute();
const collection = route.query.collection as string;
const addHTTPSetting = inject(
addHTTPSettingKey,
addHTTPSettingDefaultValue,
);
const addFolder = inject(addFolderKey, addFolderDefaultValue);
const hanldeImport = async (id: string) => {
try {
const data = (await readTextFromClipboard()) || "";
newImportDialog({
dialog,
collection,
folder: id,
data,
});
} catch (err) {
showError(message, err);
}
};
const handleExport = async (id: string) => {
try {
const apiSettings: APISetting[] = [];
const folders: APIFolder[] = [];
const name = apiFolderStore.findByID(id).name;
const folderIdList:string[] = [];
const appendChildren = (folderId: string) => {
if (folderIdList.includes(folderId)) {
return;
}
folderIdList.push(folderId);
const folder = apiFolderStore.findByID(folderId);
if (!folder) {
return;
}
folder.children.split(",").forEach((child) => {
const apiSetting = apiSettingStore.findByID(child);
if (apiSetting) {
apiSettings.push(apiSetting);
} else {
// folder
appendChildren(child);
}
});
};
appendChildren(id);
const arr: unknown[] = [];
folders.forEach((folder) => arr.push(folder));
apiSettings.forEach((apiSetting) => arr.push(apiSetting));
await writeSettingToDownload(arr, name);
message.info(i18nCollection("exportSettingsSuccess"));
} catch (err) {
showError(message, err);
}
};
const handleSelect = (key: string) => {
const { id, apiSettingType } = props;
let name = "";
let isFolder = false;
if (apiSettingType === SettingType.Folder) {
isFolder = true;
name = apiFolderStore.findByID(id).name;
} else {
name = apiSettingStore.findByID(id).name;
}
switch (key) {
case HandleKey.Delete:
{
const content = i18nCollection("deleteSettingContent").replace(
"%s",
name,
);
const d = dialog.warning({
title: i18nCollection("deleteSetting"),
content: content,
positiveText: i18nCommon("confirm"),
onPositiveClick: async () => {
d.loading = true;
try {
if (isFolder) {
await apiFolderStore.remove(id);
} else {
await apiSettingStore.remove(id);
}
} catch (err) {
showError(message, err);
}
},
});
}
break;
case HandleKey.Modify:
{
ExDialog({
dialog,
title: i18nCollection("modifySetting"),
formItems: [
{
key: "name",
label: i18nCommon("name"),
defaultValue: name,
placeholer: i18nCommon("namePlaceholder"),
rule: {
required: true,
message: i18nCommon("nameRequireError"),
trigger: "blur",
},
},
],
onConfirm: async (data) => {
try {
if (isFolder) {
await apiFolderStore.updateByID(id, data);
} else {
await apiSettingStore.updateByID(id, data);
}
} catch (err) {
showError(message, err);
}
},
});
}
break;
case HandleKey.Create:
{
addHTTPSetting(id);
}
break;
case HandleKey.CreateFolder:
{
addFolder(id);
}
break;
case HandleKey.CopyAsCURL:
{
const { req } = apiSettingStore.getHTTPRequestFillValues(id);
cookieStore
.fetch()
.then(() => {
return convertRequestToCURL(
collection,
req,
cookieStore.cookies,
);
})
.then(writeTextToClipboard)
.then(() => {
message.success(i18nCollection("copyAsCURLSuccess"));
})
.catch((err) => {
showError(message, err);
});
}
break;
case HandleKey.Pin:
{
pinRequestStore.add(collection, {
id,
});
}
break;
case HandleKey.ImportSettings:
{
hanldeImport(id);
}
break;
case HandleKey.ExportSettings:
{
handleExport(id);
}
break;
case HandleKey.Copy:
{
const setting = apiSettingStore.findByID(id);
writeTextToClipboard(JSON.stringify(setting, null, 2))
.then(() => {
message.success(i18nCollection("copySettingSuccess"));
})
.catch((err) => {
showError(message, err);
});
}
break;
default:
break;
}
};
return {
handleSelect,
};
},
render() {
const { apiSettingType } = this.$props;
const options: DropdownMixedOption[] = [
{
label: i18nCollection("modifySetting"),
key: HandleKey.Modify,
icon: () => (
),
},
];
if (apiSettingType === SettingType.Folder) {
options.unshift(
{
label: i18nCollection("newHTTPRequest"),
key: HandleKey.Create,
icon: () => (
),
},
{
label: i18nCollection("newFolder"),
key: HandleKey.CreateFolder,
icon: () => (
),
},
);
options.push(
{
label: i18nCollection("exportSettings"),
key: HandleKey.ExportSettings,
icon: () => (
),
},
{
label: i18nCollection("importSettings"),
key: HandleKey.ImportSettings,
icon: () => (
),
},
);
} else {
options.push(
{
label: i18nCollection("copyAsCURL"),
key: HandleKey.CopyAsCURL,
icon: () => (
),
},
{
label: i18nCollection("pinRequest"),
key: HandleKey.Pin,
icon: () => (
),
},
{
label: i18nCollection("copySetting"),
key: HandleKey.Copy,
icon: () => (
),
},
);
}
options.push(
{
type: "divider",
key: HandleKey.Divider,
},
{
label: i18nCollection("deleteSetting"),
key: HandleKey.Delete,
icon: () => (
),
},
);
return (
{
return {option.label};
}}
>
{
e.stopPropagation();
}}
>
);
},
});
================================================
FILE: src/components/APISettingTree/items.tsx
================================================
// API列表,实现拖动
import { defineComponent, ref, onBeforeUnmount } from "vue";
import { storeToRefs } from "pinia";
import { css } from "@linaria/core";
import { NGradientText, NInput, useMessage } from "naive-ui";
import { sortBy, uniq } from "lodash-es";
import { useRoute } from "vue-router";
import { useAPIFolderStore } from "../../stores/api_folder";
import { isMatchTextOrPinYin, showError } from "../../helpers/util";
import { useAPISettingStore, SettingType } from "../../stores/api_setting";
import { APIFolder } from "../../commands/api_folder";
import { APISetting } from "../../commands/api_setting";
import { useSettingStore } from "../../stores/setting";
import { useAPICollectionStore } from "../../stores/api_collection";
import {
nodeAddClass,
nodeGetDataValue,
nodeGetOffset,
nodeGetOffsetHeightWidth,
nodGetScrollTop,
nodeInsertAt,
nodeRemove,
nodeRemoveClass,
nodeSetStyle,
nodeHasClass,
} from "../../helpers/html";
import APISettingTreeItemDropdown from "./item_dropdown";
import { HTTPMethod } from "../../commands/http_request";
import { openFolderIcon, closeFolderIcon } from "../../icons";
const itemsWrapperClass = css`
user-select: none;
position: absolute;
top: 50px;
left: 5px;
right: 5px;
bottom: 5px;
overflow-y: auto;
overflow-x: hidden;
&.dragging {
li:hover {
background-color: rgba(255, 255, 255, 0) !important;
}
.dragItem {
&:hover {
background-color: rgba(255, 255, 255, 0.3) !important;
}
&.light:hover {
background-color: rgba(0, 0, 0, 0.3) !important;
}
}
}
ul {
margin: 0;
padding: 0;
}
li {
list-style: none;
padding: 3px 10px;
line-height: 34px;
height: 34px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
.method {
margin: 0 8px 0 5px;
font-size: 12px;
}
&.renameItem {
// 避免出现...
text-overflow: initial;
&:hover .itemDropitem {
display: none;
}
}
.renameInput {
width: 86% !important;
}
&.insertBefore {
padding-top: 2px;
border-top: 1px dashed;
}
&:hover {
cursor: pointer;
background-color: rgba(255, 255, 255, 0.1);
.itemDropitem {
display: inline;
}
}
&.selected {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
}
&.light:hover {
background-color: rgba(30, 30, 30, 0.1);
}
&.light.selected {
background-color: rgba(30, 30, 30, 0.1);
color: #000;
}
.itemDropitem {
float: right;
display: none;
}
.folder {
display: block;
float: left;
width: 30px;
height: 100%;
background-size: 20px;
background-repeat: no-repeat;
background-position: center;
&.close {
background-image: url(${closeFolderIcon});
}
&.open {
background-image: url(${openFolderIcon});
}
}
}
.n-icon {
float: left;
font-size: 16px;
line-height: 29px;
margin: 5px 5px 0 10px;
font-weight: 900;
}
`;
enum OverType {
Over = 0,
Top = 1,
Bottom = 2,
}
interface TreeItem {
id: string;
name: string;
settingType: string;
method: string;
uri: string;
children: TreeItem[];
expanded: boolean;
parent: string;
childIndex: number;
hidden: boolean;
isLastChild: boolean;
}
function getMethodColorType(method: string) {
switch (method) {
case HTTPMethod.DELETE:
return "error";
break;
case HTTPMethod.PATCH:
case HTTPMethod.PUT:
case HTTPMethod.POST:
return "success";
break;
default:
return "info";
break;
}
}
function convertToTreeItems(params: {
apiFolders: APIFolder[];
apiSettings: APISetting[];
expandedFolders: string[];
topTreeItems: string[];
keyword: string;
}): TreeItem[] {
const { apiFolders, apiSettings, expandedFolders, topTreeItems } = params;
const map = new Map();
const keyword = params.keyword.toLowerCase();
const methodReg = /"method":"(\S+?)"/;
const uriReg = /"uri":"(\S+?)"/;
apiSettings.forEach((item) => {
let method = "";
let uri = "";
if (item.setting) {
let result = methodReg.exec(item.setting);
if (result?.length === 2) {
method = result[1];
}
result = uriReg.exec(item.setting);
if (result?.length === 2) {
uri = result[1].replace(/{{\S+}}/, "");
}
}
map.set(item.id, {
method,
uri,
id: item.id,
name: item.name,
settingType: SettingType.HTTP,
children: [],
expanded: false,
parent: "",
childIndex: -1,
hidden: false,
isLastChild: false,
});
});
apiFolders.forEach((item) => {
map.set(item.id, {
id: item.id,
uri: "",
name: item.name,
settingType: SettingType.Folder,
children: [],
expanded: expandedFolders.includes(item.id),
parent: "",
childIndex: -1,
hidden: false,
method: "",
isLastChild: false,
});
});
// 记录已经设置为子元素的id
const children = [] as string[];
apiFolders.forEach((item) => {
if (!item.children) {
return;
}
const treeItem = map.get(item.id);
if (!treeItem) {
return;
}
const arr: string[] = [];
item.children.split(",").forEach((child) => {
if (!child || child === treeItem.id) {
return;
}
arr.push(child);
});
const childCount = arr.length;
arr.forEach((child, index) => {
const subItem = map.get(child);
if (!subItem) {
return;
}
subItem.parent = treeItem.id;
subItem.childIndex = treeItem.children.length;
subItem.isLastChild = index === childCount - 1;
treeItem.children.push(subItem);
children.push(child);
});
});
let result = [] as TreeItem[];
map.forEach((item, key) => {
if (children.includes(key)) {
return;
}
result.push(item);
});
if (keyword) {
const methodKeyword = keyword.toUpperCase();
const shouldBeHide = (item: TreeItem) => {
// 匹配method
// 匹配url
// 如果当前元素匹配,则其子元素展示
if (
item.method === methodKeyword ||
item.uri.toLowerCase().includes(keyword) ||
isMatchTextOrPinYin(item.name, keyword)
) {
return;
}
let hidden = true;
item.children.forEach((item) => {
shouldBeHide(item);
// 子元素有一个非hidden,则父元素非hidden
if (!item.hidden) {
hidden = false;
}
});
item.hidden = hidden;
};
result.forEach(shouldBeHide);
}
const filterVisible = (item: TreeItem) => {
if (item.hidden) {
return false;
}
item.children = item.children.filter(filterVisible);
return true;
};
result = result.filter(filterVisible);
return sortBy(result, (item) => {
return topTreeItems.indexOf(item.id);
});
}
export default defineComponent({
name: "APISettingTreeItems",
props: {
keyword: {
type: String,
default: () => "",
},
},
setup() {
const message = useMessage();
const route = useRoute();
const wrapper = ref(null);
const collection = route.query.collection as string;
const collectionStore = useAPICollectionStore();
const apiFolderStore = useAPIFolderStore();
const apiSettingStore = useAPISettingStore();
const { apiFolders } = storeToRefs(apiFolderStore);
const { expandedFolders, topTreeItems } = storeToRefs(collectionStore);
const { isDark } = storeToRefs(useSettingStore());
const { apiSettings, selectedID } = storeToRefs(apiSettingStore);
let currentTreeItems = [] as TreeItem[];
let topTreeItemIDList = [] as string[];
const renameItem = ref({
name: "",
id: "",
});
const renameValue = ref("");
const setTreeItems = (items: TreeItem[], topItems: string[]) => {
currentTreeItems = items;
topTreeItemIDList = topItems;
};
const handleClick = async (item: TreeItem) => {
try {
// folder的处理
if (item.settingType === SettingType.Folder) {
let fn = collectionStore.openFolder;
if (item.expanded) {
fn = collectionStore.closeFolder;
}
await fn(collection, item.id);
} else {
apiSettingStore.select(item.id);
}
} catch (err) {
showError(message, err);
}
};
const handleMove = async (
moveIndex: number,
targetIndex: number,
overType: OverType,
) => {
// TODO 如果是最后一个元素的处理
// isOver move 与 target 是否重叠
const moveItem = currentTreeItems[moveIndex];
const targetItem = currentTreeItems[targetIndex];
// 如果元素不存在,则忽略不处理
if (!moveItem || !targetItem) {
return;
}
// 同一个元素不处理
if (moveItem.id === targetItem.id) {
return;
}
let parentID = targetItem.parent;
let insertBefore = targetItem.id;
// 如果是最后一个元素,而且bottom
if (
targetIndex === currentTreeItems.length - 1 &&
overType === OverType.Bottom
) {
insertBefore = "";
}
if (targetItem.settingType === SettingType.Folder) {
// 拖动至文件上面,则add child
if (overType === OverType.Over) {
parentID = targetItem.id;
insertBefore = "";
} else {
// 如果folder前面是元素,而且有parent
// 且是该folder的最后元素
// 则添加至该元素所有在folder
const newTarget = currentTreeItems[targetIndex - 1];
if (newTarget && newTarget.parent && newTarget.isLastChild) {
parentID = newTarget.parent;
insertBefore = "";
}
}
}
try {
await apiFolderStore.addChild({
id: parentID,
children: [moveItem.id],
before: insertBefore,
});
if (!parentID) {
// 设置至top items
const moveItemIndex = topTreeItemIDList.indexOf(moveItem.id);
if (moveItemIndex !== -1) {
topTreeItemIDList.splice(moveItemIndex, 1);
}
const index = topTreeItemIDList.indexOf(insertBefore);
if (index === -1) {
topTreeItemIDList.push(moveItem.id);
} else {
topTreeItemIDList.splice(index, 0, moveItem.id);
}
await collectionStore.updateTopTreeItems(
collection,
uniq(topTreeItemIDList),
);
}
} catch (err) {
showError(message, err);
}
};
let target: EventTarget;
let moveTarget: EventTarget;
let originClientY = 0;
let originOffset = 0;
let targetHeight = 0;
let currentInsertIndex = -1;
let isDragging = false;
const draggingClass = "dragging";
let listItems = [] as HTMLCollection[];
let mousedownFiredAt = 0;
let maxMoveOffsetX = 0;
const handleMousemove = (e: MouseEvent) => {
// 每移动两个点再处理
if (isDragging && e.clientY % 2 !== 0) {
e.preventDefault();
return;
}
const offset = e.clientY - originClientY;
if (
!isDragging &&
Math.abs(offset) > 5 &&
Date.now() - mousedownFiredAt < 500
) {
isDragging = true;
nodeAddClass(wrapper.value, draggingClass);
nodeAddClass(document.body, "disableUserSelect");
// 提交公共方法至html无法复制(TODO确认原因)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
moveTarget = target.cloneNode(true);
nodeSetStyle(moveTarget, {
position: "absolute",
left: "0px",
right: "0px",
});
nodeAddClass(moveTarget, "dragItem");
nodeInsertAt(wrapper.value, moveTarget, 0);
}
if (isDragging) {
const top = offset + originOffset + nodGetScrollTop(wrapper.value);
const index = Math.round(top / targetHeight);
if (currentInsertIndex !== index) {
if (currentInsertIndex !== -1) {
nodeRemoveClass(listItems[currentInsertIndex], "insertBefore");
}
if (listItems.length > index) {
nodeAddClass(listItems[index], "insertBefore");
currentInsertIndex = index;
}
}
nodeSetStyle(moveTarget, {
top: `${top}px`,
});
e.preventDefault();
}
};
const handleMouseup = (e: MouseEvent) => {
document.removeEventListener("mousemove", handleMousemove);
document.removeEventListener("mouseup", handleMouseup);
nodeRemoveClass(document.body, "disableUserSelect");
if (!isDragging) {
return;
}
let overType = OverType.Bottom;
const overOffset = 5;
const offset = Math.abs(e.clientY - originClientY) % targetHeight;
// 覆盖
if (offset <= overOffset || targetHeight - offset <= overOffset) {
overType = OverType.Over;
} else if (offset < targetHeight * 0.4) {
overType = OverType.Top;
}
isDragging = false;
const moveItemIndex = Number.parseInt(
nodeGetDataValue(moveTarget, "index"),
);
const targetItemIndex = Number.parseInt(
nodeGetDataValue(listItems[currentInsertIndex], "index"),
);
nodeRemove(moveTarget);
nodeRemoveClass(listItems[currentInsertIndex], "insertBefore");
nodeRemoveClass(wrapper.value, draggingClass);
if (maxMoveOffsetX && e.clientX > maxMoveOffsetX) {
return;
}
handleMove(moveItemIndex, targetItemIndex, overType);
};
const handleMousedown = (e: MouseEvent) => {
isDragging = false;
// 无target或者点击非左键
if (!e.currentTarget || e.button > 1) {
return;
}
mousedownFiredAt = Date.now();
// TODO 此处导致无法复制,后续研究
// e.preventDefault();
currentInsertIndex = -1;
target = e.currentTarget;
originOffset =
nodeGetOffset(target).top - nodeGetOffset(wrapper.value).top;
targetHeight = nodeGetOffsetHeightWidth(target).height;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
listItems = wrapper.value.children[0].children;
originClientY = e.clientY;
document.addEventListener("mousemove", handleMousemove);
document.addEventListener("mouseup", handleMouseup);
if (wrapper.value) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
maxMoveOffsetX = wrapper.value.clientWidth as number;
}
};
const resetRename = () => {
renameValue.value = "";
renameItem.value = {
name: "",
id: "",
};
};
const handelRename = async () => {
// 无变化,无需修改
const name = renameValue.value;
const id = renameItem.value.id;
if (!name || !id) {
resetRename();
return;
}
try {
const folder = apiFolderStore.findByID(id);
if (folder) {
folder.name = name;
await apiFolderStore.updateByID(id, folder);
} else {
const apiSetting = apiSettingStore.findByID(id);
apiSetting.name = name;
await apiSettingStore.updateByID(id, apiSetting);
}
} catch (err) {
showError(message, err);
} finally {
resetRename();
}
};
const handleKeydown = (e: KeyboardEvent) => {
const key = e.key.toLowerCase();
switch (key) {
case "escape":
{
resetRename();
}
break;
case "enter":
{
handelRename();
}
break;
default:
break;
}
};
document.addEventListener("keydown", handleKeydown);
onBeforeUnmount(() => {
document.removeEventListener("keydown", handleKeydown);
});
return {
renameValue,
renameItem,
selectedID,
topTreeItems,
expandedFolders,
isDark,
apiFolders,
apiSettings,
handleClick,
handleMousedown,
handelRename,
setTreeItems,
wrapper,
};
},
render() {
const { keyword } = this.$props;
const {
apiFolders,
apiSettings,
isDark,
expandedFolders,
topTreeItems,
setTreeItems,
selectedID,
renameItem,
} = this;
const treeItems = convertToTreeItems({
apiFolders,
apiSettings,
expandedFolders,
topTreeItems,
keyword,
});
const showAllChildren = keyword.trim().length !== 0;
const itemList = [] as JSX.Element[];
// 当前展示的tree item
const currentTreeItems = [] as TreeItem[];
// 顶层元素
const topTreeItemIDList = [] as string[];
let treeItemIndex = 0;
const appendToList = (items: TreeItem[], level: number) => {
if (!items || items.length === 0) {
return;
}
items.forEach((item) => {
if (level === 0) {
topTreeItemIDList.push(item.id);
}
let folderClass = "folder";
if (item.expanded) {
folderClass += " open";
} else {
folderClass += " close";
}
let icon = ;
const isFolder = item.settingType === SettingType.Folder;
if (!isFolder) {
icon = (
{item.method || HTTPMethod.GET}
);
}
const style = {
"padding-left": `${level * 20}px`,
};
let cls = isDark ? "" : "light";
if (item.id === selectedID) {
cls += " selected";
}
if (item.id === renameItem.id) {
cls += " renameItem";
}
const onClick =
item.id !== selectedID
? () => {
if (this.renameItem.id) {
this.renameItem = {
id: "",
name: "",
};
}
this.handleClick(item);
}
: undefined;
const onDblclick = !isFolder
? (e: MouseEvent) => {
if (!nodeHasClass(e.target, "name")) {
return;
}
this.renameItem = {
id: item.id,
name: item.name,
};
}
: undefined;
currentTreeItems.push(item);
itemList.push(
{icon}
{item.id === renameItem.id && (
{
node.el?.getElementsByTagName("input")[0]?.focus();
}}
onUpdateValue={(value) => {
this.renameValue = value;
}}
onInputBlur={() => {
this.handelRename();
}}
/>
)}
{item.id !== renameItem.id && {item.name}}
,
);
treeItemIndex++;
// 未展开的则不需要展示子元素
// 而且非展示所有子元素
if (!item.expanded && !showAllChildren) {
return;
}
appendToList(item.children, level + 1);
});
};
appendToList(treeItems, 0);
setTreeItems(currentTreeItems, topTreeItemIDList);
return (
);
},
});
================================================
FILE: src/components/ExColumn.tsx
================================================
import { defineComponent, PropType, StyleValue } from "vue";
import { css } from "@linaria/core";
import { NDivider } from "naive-ui";
import { nodeInsertAt, nodeSetStyle, nodeRemove } from "../helpers/html";
const dividerClass = css`
margin: 0 !important;
height: 100% !important;
position: absolute;
`;
export default defineComponent({
name: "ExColumn",
props: {
width: {
type: Number,
default: 0,
},
left: {
type: Number,
default: 0,
},
showDivider: {
type: Boolean,
default: false,
},
onResize: {
type: Function as PropType<(value: number) => void>,
default: () => {
return (value: number) => {
console.info(value);
};
},
},
},
setup(props) {
let isDragging = false;
let originClientX = 0;
let target: EventTarget;
let moveLeft = 0;
const onMousemove = (e: MouseEvent) => {
if (!isDragging) {
return;
}
e.preventDefault();
moveLeft = e.clientX - originClientX;
nodeSetStyle(target, {
left: `${e.clientX}px`,
});
};
const onMouseup = () => {
if (props.onResize) {
props.onResize(moveLeft);
}
moveLeft = 0;
isDragging = false;
document.removeEventListener("mousemove", onMousemove);
document.removeEventListener("mouseup", onMouseup);
if (target) {
nodeRemove(target);
}
};
const onMousedown = (e: MouseEvent) => {
isDragging = false;
if (!e.currentTarget) {
return;
}
originClientX = e.clientX;
isDragging = true;
document.addEventListener("mousemove", onMousemove);
document.addEventListener("mouseup", onMouseup);
// 提交公共方法至html无法复制(TODO确认原因)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
target = e.currentTarget.cloneNode(true);
nodeSetStyle(target, {
left: `${originClientX}px`,
width: "2px",
zIndex: "9",
});
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
nodeInsertAt(e.currentTarget.parentNode.parentNode, target, 0);
};
return {
onMousedown,
};
},
render() {
const { left, width, showDivider } = this.$props;
const { $slots } = this;
const style: StyleValue = {
position: "absolute",
top: "0px",
bottom: "0px",
left: `${left}px`,
width: `${width}px`,
overflow: "hidden",
};
if (!width) {
delete style.width;
style.right = "0px";
}
const divider = showDivider && (
);
return (
{divider}
{$slots.default && $slots.default()}
);
},
});
================================================
FILE: src/components/ExCookieEditor.tsx
================================================
import { defineComponent, PropType, ref } from "vue";
import {
NForm,
NFormItem,
NInput,
NP,
NGrid,
NGi,
NDatePicker,
useMessage,
NButton,
FormInst,
} from "naive-ui";
import dayjs from "dayjs";
import { i18nCookie, i18nCommon } from "../i18n";
import { Cookie } from "../commands/cookies";
import { useCookieStore } from "../stores/cookie";
import { newRequireRules, showError } from "../helpers/util";
export default defineComponent({
name: "ExCookieEditor",
props: {
cookie: {
type: Object as PropType,
required: true,
},
onBack: {
type: Function as PropType<() => void>,
default: () => {
console.info("back");
},
},
},
setup(props) {
const message = useMessage();
const cookieStore = useCookieStore();
const formRef = ref(null);
const updateValues = ref({
name: props.cookie.name,
path: props.cookie.path,
domain: props.cookie.domain,
value: props.cookie.value,
expires: props.cookie.expires,
});
const cookie = Object.assign({}, props.cookie);
const update = async () => {
const { value, expires, name, path, domain } = updateValues.value;
try {
await new Promise((resolve, reject) => {
if (!formRef.value) {
return reject(new Error("form ref is null"));
}
formRef.value.validate((errors) => {
if (!errors) {
resolve(null);
return;
}
reject(errors[0][0]);
});
});
cookie.value = value;
cookie.expires = expires;
// 新增
if (!cookie.name) {
cookie.name = name;
cookie.path = path || "/";
cookie.domain = domain || "";
}
await cookieStore.addOrUpdate(cookie);
if (props.onBack) {
props.onBack();
}
} catch (err) {
showError(message, err);
}
};
return {
updateValues,
update,
formRef,
};
},
render() {
const { cookie } = this.$props;
let defaultExpires = null;
if (cookie.expires) {
defaultExpires = dayjs(cookie.expires).unix() * 1000;
}
const shortcuts = {
[i18nCookie("neverExpired")]: 32503651200000,
};
const isAdd = !cookie.name;
const rules = newRequireRules([
"name",
"value",
"path",
"domain",
"expires",
]);
return (
{!isAdd && {cookie.name}}
{isAdd && (
{
this.updateValues.name = value;
}}
/>
)}
{!isAdd && {cookie.path}}
{isAdd && (
{
this.updateValues.path = value;
}}
/>
)}
{!isAdd && {cookie.domain}}
{isAdd && (
{
this.updateValues.domain = value;
}}
/>
)}
{
this.updateValues.value = value;
}}
/>
{
this.updateValues.expires = dayjs(value).toString();
}}
clearable
/>
{
this.update();
}}
>
{i18nCommon("confirm")}
{
this.$props.onBack();
}}
>
{i18nCommon("back")}
);
},
});
================================================
FILE: src/components/ExDeleteCheck.tsx
================================================
import { defineComponent, ref, PropType } from "vue";
import { NButton, NIcon } from "naive-ui";
import { AlertCircleOutline, TrashOutline } from "@vicons/ionicons5";
import { css } from "@linaria/core";
const checkClass = css`
color: #f2c97d;
font-weight: 900;
`;
// 两次短时间点击则清除
export default defineComponent({
name: "ExDeleteCheck",
props: {
onConfirm: {
type: Function as PropType<() => void>,
required: true,
},
},
setup(props) {
const deleting = ref(false);
const handleClick = () => {
if (!deleting.value) {
deleting.value = true;
} else if (props.onConfirm) {
props.onConfirm();
}
};
return {
handleClick,
deleting,
};
},
render() {
const { deleting } = this;
return (
{
this.handleClick();
}}
>
{!deleting && }
{deleting && }
);
},
});
================================================
FILE: src/components/ExDialog.tsx
================================================
import { editor } from "monaco-editor/esm/vs/editor/editor.api";
import { DialogApiInjection } from "naive-ui/es/dialog/src/DialogProvider";
import { defineComponent, onBeforeUnmount, ref, PropType } from "vue";
import { i18nCollection, i18nCommon } from "../i18n";
import { useSettingStore } from "../stores/setting";
import { useEnvironmentStore } from "../stores/environment";
import ExForm, { ExFormItem, ExUpdateData } from "./ExForm";
import { createEditor } from "../helpers/editor";
import { css } from "@linaria/core";
import {
NButton,
NIcon,
NTabPane,
NTabs,
NText,
NUpload,
NUploadDragger,
UploadFileInfo,
useMessage,
} from "naive-ui";
import { useAPISettingStore } from "../stores/api_setting";
import { useAPIFolderStore } from "../stores/api_folder";
import {
getNormalDialogStyle,
isJSON,
jsonFormat,
showError,
delay,
} from "../helpers/util";
import { CloudUploadOutline } from "@vicons/ionicons5";
import { importAPI, ImportCategory } from "../commands/import_api";
interface OnConfirm {
(data: ExUpdateData): Promise;
}
interface DialogOption {
dialog: DialogApiInjection;
title: string;
enterTriggerSubmit?: boolean;
formItems: ExFormItem[];
onConfirm: OnConfirm;
}
interface ImportDialogOption {
dialog: DialogApiInjection;
collection: string;
folder?: string;
data: string;
}
export default function newDialog(option: DialogOption) {
const { dialog, formItems } = option;
const d = dialog.info({
title: option.title,
autoFocus: true,
closable: false,
content: () => (
{
await option.onConfirm(data);
d.destroy();
// 增加延时,否则快速点击时会触发两次
await delay(100);
}}
/>
),
});
}
const importWrapperHeight = 300;
const codeEditorClass = css`
.codeEditor {
height: ${importWrapperHeight}px;
overflow-y: auto;
}
`;
const ImportEditor = defineComponent({
name: "ImportEditor",
props: {
data: {
type: String,
default: () => "",
},
collection: {
type: String,
required: true,
},
folder: {
type: String,
default: () => "",
},
onConfirm: {
type: Function as PropType<() => void>,
required: true,
},
},
setup(props) {
const settingStore = useSettingStore();
const apiSettingStore = useAPISettingStore();
const apiFolderStore = useAPIFolderStore();
const message = useMessage();
const currentTab = ref(ImportCategory.Text);
const fileData = ref("");
let editorIns: editor.IStandaloneCodeEditor | null;
const destroyEditor = () => {
if (editorIns) {
editorIns = null;
}
};
const codeEditor = ref();
const initEditor = () => {
if (editorIns) {
return;
}
if (codeEditor.value) {
editorIns = createEditor({
dom: codeEditor.value,
isDark: settingStore.isDark,
});
}
if (!editorIns) {
return;
}
const data = isJSON(props.data) ? jsonFormat(props.data) : "";
editorIns.setValue(data);
const setFocus = () => {
editorIns?.focus();
};
setTimeout(setFocus, 50);
};
const processing = ref(false);
const handleImport = async () => {
if (processing.value) {
return;
}
processing.value = true;
try {
if (currentTab.value === ImportCategory.Text) {
if (editorIns) {
fileData.value = editorIns.getValue();
} else {
fileData.value = "";
}
}
const topIDList = await importAPI({
category: currentTab.value,
collection: props.collection,
fileData: fileData.value.trim(),
});
// 如果指定了目录
if (props.folder && topIDList.length) {
await apiFolderStore.addChild({
id: props.folder,
children: topIDList,
});
}
// 重新加载数据,触发页面刷新
await apiFolderStore.fetch(props.collection);
await apiSettingStore.fetch(props.collection);
await useEnvironmentStore().fetch(props.collection);
message.info(i18nCollection("importSuccess"));
if (props.onConfirm) {
props.onConfirm();
}
} catch (err) {
showError(message, err);
} finally {
processing.value = false;
}
};
const handleReadFile = (blob: Blob) => {
const r = new FileReader();
r.onload = () => {
fileData.value = r.result as string;
};
r.onerror = () => {
fileData.value = "";
showError(message, new Error("read file fail"));
};
r.readAsText(blob);
};
onBeforeUnmount(() => {
destroyEditor();
});
return {
currentTab,
fileData,
destroyEditor,
initEditor,
handleReadFile,
handleImport,
processing,
codeEditor,
};
},
render() {
const { currentTab } = this;
const uploadWrapper = (
{
this.handleReadFile(data.fileList[0].file as Blob);
}}
>
{i18nCollection("dragUploadTips")}
);
return (
{
this.fileData = "";
this.currentTab = value;
}}
>
{
this.initEditor();
}}
onVnodeUnmounted={() => {
this.destroyEditor();
}}
>
{uploadWrapper}
{
this.handleImport();
}}
>
{i18nCommon("confirm")}
);
},
});
export function newImportDialog(option: ImportDialogOption) {
const { dialog, data, collection, folder } = option;
const d = dialog.info({
title: i18nCollection("importSettings"),
closable: false,
style: getNormalDialogStyle(),
content: () => (
{
d.destroy();
}}
/>
),
});
}
================================================
FILE: src/components/ExForm.tsx
================================================
import { defineComponent, PropType, ref } from "vue";
import {
FormInst,
FormRules,
FormItemRule,
NForm,
NFormItem,
NInput,
NButton,
useMessage,
} from "naive-ui";
import { get, isArray } from "lodash-es";
import { i18nCommon } from "../i18n";
import { showError } from "../helpers/util";
export interface ExUpdateData {
[key: string]: unknown;
}
export interface ExOnSubmit {
(data: ExUpdateData): Promise;
}
export interface ExFormItem {
inputType?: "textarea" | "text" | "password";
key: string;
label: string;
placeholer: string;
defaultValue?: string;
rule?: FormItemRule;
}
export default defineComponent({
name: "ExForm",
props: {
formItems: {
type: Array as PropType,
required: true,
},
onSubmit: {
type: Function as PropType,
required: true,
},
enterTriggerSubmit: {
type: Boolean,
default: () => false,
},
},
setup(props) {
const message = useMessage();
const formRef = ref(null);
const data: ExUpdateData = {};
const rules: FormRules = {};
props.formItems.forEach((item) => {
if (item.defaultValue) {
data[item.key] = item.defaultValue;
}
if (item.rule) {
rules[item.key] = item.rule;
}
});
const formValue = ref(data);
const submitting = ref(false);
const submit = async () => {
if (submitting.value) {
return;
}
submitting.value = true;
try {
await formRef.value?.validate();
if (props.onSubmit) {
await props.onSubmit(data);
}
} catch (errors) {
let err = errors;
if (isArray(errors)) {
err = get(errors, "[0][0].message");
}
showError(message, err);
} finally {
submitting.value = false;
}
};
return {
submitting,
formValue,
formRef,
rules,
submit,
};
},
render() {
const { formValue, rules, submit, submitting } = this;
const { formItems } = this.$props;
const lastIndex = formItems.length - 1;
const items = formItems.map((item, index) => {
const isAddEvent = index === lastIndex || this.$props.enterTriggerSubmit;
const onKeyup = isAddEvent
? (e: KeyboardEvent) => {
if (e.key.toLocaleLowerCase() === "enter") {
submit();
}
}
: undefined;
return (
{
formValue[item.key] = value;
}}
/>
);
});
return (
{items}
submit()}>
{i18nCommon("confirm")}
);
},
});
================================================
FILE: src/components/ExKeyValue.tsx
================================================
import { ulid } from "ulid";
import { NButton, NGi, NGrid, NIcon, NInput, useMessage } from "naive-ui";
import { defineComponent, PropType, ref } from "vue";
import { css } from "@linaria/core";
import {
CheckboxOutline,
DocumentOutline,
SquareOutline,
} from "@vicons/ionicons5";
import { debounce } from "lodash-es";
import { open } from "@tauri-apps/api/dialog";
import { downloadDir } from "@tauri-apps/api/path";
import ExDeleteCheck from "./ExDeleteCheck";
import { KVParam } from "../commands/interface";
import { i18nCollection, i18nCommon } from "../i18n";
import { padding } from "../constants/style";
import { showError } from "../helpers/util";
const kvClass = css`
.item {
margin-bottom: ${padding}px;
}
.n-input--autosize {
width: 100%;
}
.btns {
float: right;
margin-left: 5px;
.n-button {
padding: 0 8px !important;
margin-left: 2px;
}
i {
font-size: 16px;
}
}
.kv {
margin-right: 75px;
&.withFile {
margin-right: 110px;
}
}
`;
export enum HandleOptionCategory {
Update = "update",
Add = "add",
Delete = "delete",
}
export interface HandleOption {
category: string;
index: number;
param?: KVParam;
params: KVParam[];
}
type KVItem = {
id: string;
isNew: boolean;
} & KVParam;
export default defineComponent({
name: "ExKeyValue",
props: {
params: {
type: Array as PropType,
required: true,
},
spans: {
type: Array as PropType,
default: () => [12, 12],
},
onHandleParam: {
type: Function as PropType<(opt: HandleOption) => void>,
required: true,
},
typeList: {
type: Array as PropType<("textarea" | "text" | "password")[]>,
default: () => ["textarea", "textarea"],
},
supportFileSelect: {
type: Boolean,
default: () => false,
},
},
setup(props) {
const message = useMessage();
const arr = props.params.map((item) => {
return Object.assign(
{
id: ulid(),
isNew: false,
},
item,
);
});
const kvList = ref(arr as KVItem[]);
const addParams = (item: KVItem) => {
kvList.value.push(item);
};
const handle = (opt: HandleOption) => {
if (props.onHandleParam) {
props.onHandleParam(opt);
}
};
const toggleEnabled = (index: number) => {
if (index >= kvList.value.length) {
return;
}
const item = kvList.value[index];
item.enabled = !item.enabled;
if (item.key && item.value) {
handle({
category: HandleOptionCategory.Update,
param: item,
index,
params: kvList.value,
});
}
};
const handleUpdate = (index: number) => {
if (index >= kvList.value.length) {
return;
}
const item = kvList.value[index];
let category = HandleOptionCategory.Update;
if (item.isNew) {
category = HandleOptionCategory.Add;
item.isNew = false;
}
handle({
category,
param: item,
index,
params: kvList.value,
});
};
const deleteParams = (index: number) => {
const items = kvList.value.splice(index, 1);
// 如果是新元素未添加至数据库的,则忽略
if (items.length && items[0].isNew) {
return;
}
handle({
category: HandleOptionCategory.Delete,
index,
params: kvList.value,
});
};
const selectFile = async (index: number) => {
if (index >= kvList.value.length) {
return;
}
try {
const selected = await open({
title: i18nCommon("selectFile"),
multiple: false,
defaultPath: await downloadDir(),
});
if (selected) {
const item = kvList.value[index];
item.value = ("file://" + selected) as string;
item.id = ulid();
kvList.value[index] = item;
}
handleUpdate(index);
} catch (err) {
showError(message, err);
}
};
return {
kvList,
handleUpdate,
selectFile,
toggleEnabled,
deleteParams,
addParams,
};
},
render() {
const { spans, typeList, supportFileSelect } = this.$props;
const { kvList } = this;
const arr = kvList.slice(0);
const lastItem: KVItem = {
id: ulid(),
key: "",
value: "",
enabled: true,
isNew: true,
};
arr.push(lastItem);
const namePlaceholder = i18nCollection("namePlaceholder");
const valuePlaceholder = i18nCollection("valuePlaceholder");
const size = arr.length;
const inputDebounce = 200;
const kvCls = ["kv"];
if (supportFileSelect) {
kvCls.push("withFile");
}
const list = arr.map((item, index) => {
const isLast = index === size - 1;
const handleFocus = () => {
// 点击最后一个元素,则添加
if (isLast) {
this.addParams(lastItem);
}
};
return (
{!isLast && (
{supportFileSelect && (
{
this.selectFile(index);
}}
>
)}
{
this.toggleEnabled(index);
}}
>
{item.enabled && }
{!item.enabled && }
{
this.deleteParams(index);
}}
/>
)}
{
arr[index].key = value;
this.handleUpdate(index);
}, inputDebounce)}
>
{
arr[index].value = value;
this.handleUpdate(index);
}, inputDebounce)}
>
);
});
return {list}
;
},
});
================================================
FILE: src/components/ExLoading.tsx
================================================
import { defineComponent, PropType, StyleValue } from "vue";
import { css } from "@linaria/core";
import { i18nCommon } from "../i18n";
import { useSettingStore } from "../stores/setting";
import { NText } from "naive-ui";
const loadingClass = css`
text-align: center;
height: 30px;
line-height: 30px;
`;
const loadingTextClass = css`
margin-left: 5px;
`;
export default defineComponent({
name: "ExLoading",
props: {
style: {
type: Object as PropType,
default: () => {
return {
padding: "30px 0",
};
},
},
},
setup() {
const settingStore = useSettingStore();
let color = "#000";
if (settingStore.isDark) {
color = "#fff";
}
return {
color,
};
},
render() {
const { color } = this;
const { style } = this.$props;
return (
);
},
});
================================================
FILE: src/components/ExPreview.tsx
================================================
import { defineComponent } from "vue";
import { css } from "@linaria/core";
const wrapperClass = css`
padding: 15px;
img,
span,
iframe {
max-width: 100%;
display: block;
margin: auto;
}
`;
export function isSupportPreview(contentType: string) {
const reg = /image|pdf/i;
return reg.test(contentType);
}
export default defineComponent({
name: "ExPreview",
props: {
contentType: {
type: String,
required: true,
},
data: {
type: String,
required: true,
},
},
render() {
const { contentType, data } = this.$props;
let dom = Not Support
;
const src = `data:${contentType};base64,${data}`;
if (contentType.includes("image")) {
dom =
;
} else {
const height = window.innerHeight || 700;
dom = ;
}
return {dom}
;
},
});
================================================
FILE: src/components/ExTimer.tsx
================================================
import { defineComponent, onBeforeUnmount, ref } from "vue";
import { css } from "@linaria/core";
const timerClass = css`
font-variant-numeric: tabular-nums;
font-weight: 400;
`;
export default defineComponent({
name: "ExTimer",
setup() {
const now = Date.now();
const seconds = ref(0);
const timer = setInterval(() => {
seconds.value = Date.now() - now;
}, 100);
onBeforeUnmount(() => clearInterval(timer));
return {
seconds,
};
},
render() {
const { seconds } = this;
return {(seconds / 1000).toFixed(1)} s;
},
});
================================================
FILE: src/constants/common.ts
================================================
export const appName = "Cyber API";
================================================
FILE: src/constants/handle_key.ts
================================================
export enum HandleKey {
Delete = "delete",
Modify = "modify",
Create = "create",
CreateFolder = "createFolder",
Copy = "copy",
CopyAsCURL = "copyAsCURL",
Pin = "pin",
Divider = "divider",
ImportSettings = "importSettings",
ExportSettings = "exportSettings",
}
================================================
FILE: src/constants/provide.ts
================================================
export const addHTTPSettingKey = "addHTTPSetting";
export const addHTTPSettingDefaultValue = (folder: string) => {
console.warn(`add http setting is null, parent:${folder}`);
};
export const addFolderKey = "addFolder";
export const addFolderDefaultValue = (parentFolder: string) => {
console.warn(`add folder is null, parent:${parentFolder}`);
};
================================================
FILE: src/constants/style.ts
================================================
export const mainHeaderHeight = 50;
export const padding = 10;
export const margin = 20;
================================================
FILE: src/env.d.ts
================================================
///
declare module "*.vue" {
import type { DefineComponent } from "vue";
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>;
export default component;
}
================================================
FILE: src/event.ts
================================================
import { appWindow } from "@tauri-apps/api/window";
import { hide } from "@tauri-apps/api/app";
import { isMacOS } from "./helpers/util";
export async function initWindowEvent() {
if (!(await isMacOS())) {
return;
}
appWindow.onCloseRequested((e) => {
e.preventDefault();
hide();
});
}
================================================
FILE: src/helpers/curl.js
================================================
import * as words from "shellwords";
import { forEach } from "lodash-es";
import { isJSON } from "./util";
export default function(s) {
const params = parse(s);
let contentType = "";
const headers = [];
forEach(params.header, (value, key) => {
if (key.toLowerCase() === "content-type") {
contentType = value;
return;
}
headers.push({
key,
value,
enabled: true,
});
});
if (!contentType && isJSON(params.body)) {
contentType = "application/json";
}
const index = contentType.indexOf(";");
if (index !== -1) {
contentType = contentType.substring(0, index);
}
const result = new URL(params.url);
const query = [];
result.searchParams.forEach((value, key) => {
query.push({
key,
value,
enabled: true,
});
});
return {
method: params.method,
uri: `${result.origin}${result.pathname}`,
body: params.body,
contentType,
headers,
query,
}
}
function parse(s) {
if (0 != s.indexOf('curl ')) return
var args = rewrite(words.split(s))
var out = { method: 'GET', header: {} }
var state = ''
args.forEach(function (arg) {
switch (true) {
case isURL(arg):
out.url = arg
break;
case arg == '-A' || arg == '--user-agent':
state = 'user-agent'
break;
case arg == '-H' || arg == '--header':
state = 'header'
break;
case arg == '-d' || arg == '--data' || arg == '--data-ascii':
state = 'data'
break;
case arg == '-u' || arg == '--user':
state = 'user'
break;
case arg == '-I' || arg == '--head':
out.method = 'HEAD'
break;
case arg == '-X' || arg == '--request':
state = 'method'
break;
case arg == '-b' || arg == '--cookie':
state = 'cookie'
break;
case arg == '--compressed':
out.header['Accept-Encoding'] = out.header['Accept-Encoding'] || 'deflate, gzip'
break;
case !!arg:
switch (state) {
case 'header':
var field = parseField(arg)
out.header[field[0]] = field[1]
state = ''
break;
case 'user-agent':
out.header['User-Agent'] = arg
state = ''
break;
case 'data':
if (out.method == 'GET' || out.method == 'HEAD') out.method = 'POST'
out.header['Content-Type'] = out.header['Content-Type'] || 'application/x-www-form-urlencoded'
out.body = out.body
? out.body + '&' + arg
: arg
state = ''
break;
case 'user':
out.header['Authorization'] = 'Basic ' + window.btoa(arg)
state = ''
break;
case 'method':
out.method = arg
state = ''
break;
case 'cookie':
out.header['Set-Cookie'] = arg
state = ''
break;
}
break;
}
})
return out
}
/**
* Rewrite args for special cases such as -XPUT.
*/
function rewrite(args) {
return args.reduce(function (args, a) {
if (0 == a.indexOf('-X')) {
args.push('-X')
args.push(a.slice(2))
} else {
args.push(a)
}
return args
}, [])
}
/**
* Parse header field.
*/
function parseField(s) {
return s.split(/: ?(.+)/)
}
/**
* Check if `s` looks like a url.
*/
function isURL(s) {
// TODO: others at some point
return /^https?:\/\//.test(s)
}
================================================
FILE: src/helpers/editor.ts
================================================
import { editor } from "monaco-editor/esm/vs/editor/editor.api";
export function createEditor(params: {
isDark: boolean;
readonly?: boolean;
dom: HTMLElement;
}) {
// * The current out-of-the-box available themes are: 'vs' (default), 'vs-dark', 'hc-black', 'hc-light.
const e = editor.create(params.dom, {
readOnly: params.readonly || false,
language: "json",
theme: params.isDark ? "vs-dark" : "vs",
automaticLayout: true,
});
e.updateOptions({
fontSize: 14,
lineNumbersMinChars: 4,
wordWrap: "on",
});
return e;
}
// 替换内容
export function replaceContent(
editor: editor.IStandaloneCodeEditor | null,
content: string,
) {
if (!editor) {
return;
}
editor.setValue(content || "");
}
================================================
FILE: src/helpers/hot_key.ts
================================================
const metaKey = "⌘";
const shiftKey = "⇧";
function match(hotKey: string, e: KeyboardEvent) {
let matched = true;
hotKey.split(" ").forEach((key) => {
switch (key) {
case metaKey: {
if (!e.metaKey) {
matched = false;
}
break;
}
case shiftKey: {
if (!e.shiftKey) {
matched = false;
}
break;
}
default: {
if (key.toLowerCase() !== e.key) {
matched = false;
}
break;
}
}
});
return matched;
}
export function hotKeyCreateHTTPSetting() {
return `${metaKey} N`;
}
export function hotKeyMatchCreateHTTPSetting(e: KeyboardEvent) {
return match(hotKeyCreateHTTPSetting(), e);
}
export function hotKeyCreateFolder() {
return `${shiftKey} ${metaKey} N`;
}
export function hotKeyMatchCreateFolder(e: KeyboardEvent) {
return match(hotKeyCreateFolder(), e);
}
================================================
FILE: src/helpers/html.ts
================================================
import { forEach } from "lodash-es";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
function nodeRemove(node) {
if (node && node.parentElement !== null) {
node.parentElement.removeChild(node);
}
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
function nodeInsertAt(fatherNode, node, position) {
const refNode =
position === 0
? fatherNode.children[0]
: fatherNode.children[position - 1].nextSibling;
fatherNode.insertBefore(node, refNode);
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
function nodeInsertBefore(node, beforeNode) {
const parent = beforeNode.parentNode;
let found = false;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
forEach(parent.children, (item) => {
if (found) {
return;
}
if (item === beforeNode) {
parent.insertBefore(node, item);
found = true;
}
});
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
function nodeSetStyle(node, style) {
Object.assign(node.style, style);
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
function nodeGetOffset(node) {
if (!node) {
return {
left: 0,
top: 0,
};
}
const rect = node.getBoundingClientRect();
return {
left: rect.left,
top: rect.top,
};
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
function nodGetScrollTop(node) {
if (!node) {
return 0;
}
return node.scrollTop;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
function nodeAddClass(node, cls) {
if (!node || !cls) {
return;
}
node.classList.add(cls);
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
function nodeHasClass(node, cls) {
if (!node || !cls) {
return false;
}
return node.classList.contains(cls);
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
function nodeRemoveClass(node, cls) {
if (!node || !cls) {
return;
}
node.classList.remove(cls);
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
function nodeGetOffsetHeightWidth(node) {
if (!node) {
return {
height: 0,
width: 0,
};
}
return {
height: node.offsetHeight,
width: node.offsetWidth,
};
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
function nodeGetDataValue(node, key) {
if (!node) {
return "";
}
return node.dataset[key] as string;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
function nodeGetTagName(node) {
return node.tagName;
}
export {
nodeInsertAt,
nodeRemove,
nodeSetStyle,
nodeInsertBefore,
nodeGetOffset,
nodGetScrollTop,
nodeAddClass,
nodeRemoveClass,
nodeHasClass,
nodeGetOffsetHeightWidth,
nodeGetDataValue,
nodeGetTagName,
};
================================================
FILE: src/helpers/pinyin.ts
================================================
function newPinYinMap() {
const data: { [key: string]: unknown } = {
a: "阿啊呵腌嗄吖锕",
e: "额阿俄恶鹅遏鄂厄饿峨扼娥鳄哦蛾噩愕讹锷垩婀鹗萼谔莪腭锇颚呃阏屙苊轭",
ai: "爱埃艾碍癌哀挨矮隘蔼唉皑哎霭捱暧嫒嗳瑷嗌锿砹",
ei: "诶",
xi: "系西席息希习吸喜细析戏洗悉锡溪惜稀袭夕洒晰昔牺腊烯熙媳栖膝隙犀蹊硒兮熄曦禧嬉玺奚汐徙羲铣淅嘻歙熹矽蟋郗唏皙隰樨浠忾蜥檄郄翕阋鳃舾屣葸螅咭粞觋欷僖醯鼷裼穸饩舄禊诶菥蓰",
yi: "一以已意议义益亿易医艺食依移衣异伊仪宜射遗疑毅谊亦疫役忆抑尾乙译翼蛇溢椅沂泄逸蚁夷邑怡绎彝裔姨熠贻矣屹颐倚诣胰奕翌疙弈轶蛾驿壹猗臆弋铱旖漪迤佚翊诒怿痍懿饴峄揖眙镒仡黟肄咿翳挹缢呓刈咦嶷羿钇殪荑薏蜴镱噫癔苡悒嗌瘗衤佾埸圯舣酏劓",
an: "安案按岸暗鞍氨俺胺铵谙庵黯鹌桉埯犴揞厂广",
han: "厂汉韩含旱寒汗涵函喊憾罕焊翰邯撼瀚憨捍酣悍鼾邗颔蚶晗菡旰顸犴焓撖",
ang: "昂仰盎肮",
ao: "奥澳傲熬凹鳌敖遨鏖袄坳翱嗷拗懊岙螯骜獒鏊艹媪廒聱",
wa: "瓦挖娃洼袜蛙凹哇佤娲呙腽",
yu: "于与育余预域予遇奥语誉玉鱼雨渔裕愈娱欲吁舆宇羽逾豫郁寓吾狱喻御浴愉禹俞邪榆愚渝尉淤虞屿峪粥驭瑜禺毓钰隅芋熨瘀迂煜昱汩於臾盂聿竽萸妪腴圄谕觎揄龉谀俣馀庾妤瘐鬻欤鹬阈嵛雩鹆圉蜮伛纡窬窳饫蓣狳肀舁蝓燠",
niu: "牛纽扭钮拗妞忸狃",
o: "哦噢喔",
ba: "把八巴拔伯吧坝爸霸罢芭跋扒叭靶疤笆耙鲅粑岜灞钯捌菝魃茇",
pa: "怕帕爬扒趴琶啪葩耙杷钯筢",
pi: "被批副否皮坏辟啤匹披疲罢僻毗坯脾譬劈媲屁琵邳裨痞癖陂丕枇噼霹吡纰砒铍淠郫埤濞睥芘蚍圮鼙罴蜱疋貔仳庀擗甓陴",
bi: "比必币笔毕秘避闭佛辟壁弊彼逼碧鼻臂蔽拂泌璧庇痹毙弼匕鄙陛裨贲敝蓖吡篦纰俾铋毖筚荸薜婢哔跸濞秕荜愎睥妣芘箅髀畀滗狴萆嬖襞舭",
bai: "百白败摆伯拜柏佰掰呗擘捭稗",
bo: "波博播勃拨薄佛伯玻搏柏泊舶剥渤卜驳簿脖膊簸菠礴箔铂亳钵帛擘饽跛钹趵檗啵鹁擗踣",
bei: "北被备倍背杯勃贝辈悲碑臂卑悖惫蓓陂钡狈呗焙碚褙庳鞴孛鹎邶鐾",
ban: "办版半班般板颁伴搬斑扮拌扳瓣坂阪绊钣瘢舨癍",
pan: "判盘番潘攀盼拚畔胖叛拌蹒磐爿蟠泮袢襻丬",
bin: "份宾频滨斌彬濒殡缤鬓槟摈膑玢镔豳髌傧",
bang: "帮邦彭旁榜棒膀镑绑傍磅蚌谤梆浜蒡",
pang: "旁庞乓磅螃彷滂逄耪",
beng: "泵崩蚌蹦迸绷甭嘣甏堋",
bao: "报保包宝暴胞薄爆炮饱抱堡剥鲍曝葆瀑豹刨褒雹孢苞煲褓趵鸨龅勹",
bu: "不部步布补捕堡埔卜埠簿哺怖钚卟瓿逋晡醭钸",
pu: "普暴铺浦朴堡葡谱埔扑仆蒲曝瀑溥莆圃璞濮菩蹼匍噗氆攵镨攴镤",
mian: "面棉免绵缅勉眠冕娩腼渑湎沔黾宀眄",
po: "破繁坡迫颇朴泊婆泼魄粕鄱珀陂叵笸泺皤钋钷",
fan: "反范犯繁饭泛翻凡返番贩烦拚帆樊藩矾梵蕃钒幡畈蘩蹯燔",
fu: "府服副负富复福夫妇幅付扶父符附腐赴佛浮覆辅傅伏抚赋辐腹弗肤阜袱缚甫氟斧孚敷俯拂俘咐腑孵芙涪釜脯茯馥宓绂讣呋罘麸蝠匐芾蜉跗凫滏蝮驸绋蚨砩桴赙菔呒趺苻拊阝鲋怫稃郛莩幞祓艴黻黼鳆",
ben: "本体奔苯笨夯贲锛畚坌",
feng: "风丰封峰奉凤锋冯逢缝蜂枫疯讽烽俸沣酆砜葑唪",
bian: "变便边编遍辩鞭辨贬匾扁卞汴辫砭苄蝙鳊弁窆笾煸褊碥忭缏",
pian: "便片篇偏骗翩扁骈胼蹁谝犏缏",
zhen: "镇真针圳振震珍阵诊填侦臻贞枕桢赈祯帧甄斟缜箴疹砧榛鸩轸稹溱蓁胗椹朕畛浈",
biao: "表标彪镖裱飚膘飙镳婊骠飑杓髟鳔灬瘭",
piao: "票朴漂飘嫖瓢剽缥殍瞟骠嘌莩螵",
huo: "和活或货获火伙惑霍祸豁嚯藿锪蠖钬耠镬夥灬劐攉",
bie: "别鳖憋瘪蹩",
min: "民敏闽闵皿泯岷悯珉抿黾缗玟愍苠鳘",
fen: "分份纷奋粉氛芬愤粪坟汾焚酚吩忿棼玢鼢瀵偾鲼",
bing: "并病兵冰屏饼炳秉丙摒柄槟禀枋邴冫",
geng: "更耕颈庚耿梗埂羹哽赓绠鲠",
fang: "方放房防访纺芳仿坊妨肪邡舫彷枋鲂匚钫",
xian: "现先县见线限显险献鲜洗宪纤陷闲贤仙衔掀咸嫌掺羡弦腺痫娴舷馅酰铣冼涎暹籼锨苋蚬跹岘藓燹鹇氙莶霰跣猃彡祆筅",
fou: "不否缶",
ca: "拆擦嚓礤",
cha: "查察差茶插叉刹茬楂岔诧碴嚓喳姹杈汊衩搽槎镲苴檫馇锸猹",
cai: "才采财材菜彩裁蔡猜踩睬",
can: "参残餐灿惨蚕掺璨惭粲孱骖黪",
shen: "信深参身神什审申甚沈伸慎渗肾绅莘呻婶娠砷蜃哂椹葚吲糁渖诜谂矧胂",
cen: "参岑涔",
san: "三参散伞叁糁馓毵",
cang: "藏仓苍沧舱臧伧",
zang: "藏脏葬赃臧奘驵",
chen: "称陈沈沉晨琛臣尘辰衬趁忱郴宸谌碜嗔抻榇伧谶龀肜",
cao: "草操曹槽糙嘈漕螬艚屮",
ce: "策测册侧厕栅恻",
ze: "责则泽择侧咋啧仄箦赜笮舴昃迮帻",
zhai: "债择齐宅寨侧摘窄斋祭翟砦瘵哜",
dao: "到道导岛倒刀盗稻蹈悼捣叨祷焘氘纛刂帱忉",
ceng: "层曾蹭噌",
zha: "查扎炸诈闸渣咋乍榨楂札栅眨咤柞喳喋铡蚱吒怍砟揸痄哳齄",
chai: "差拆柴钗豺侪虿瘥",
ci: "次此差词辞刺瓷磁兹慈茨赐祠伺雌疵鹚糍呲粢",
zi: "资自子字齐咨滋仔姿紫兹孜淄籽梓鲻渍姊吱秭恣甾孳訾滓锱辎趑龇赀眦缁呲笫谘嵫髭茈粢觜耔",
cuo: "措错磋挫搓撮蹉锉厝嵯痤矬瘥脞鹾",
chan: "产单阐崭缠掺禅颤铲蝉搀潺蟾馋忏婵孱觇廛谄谗澶骣羼躔蒇冁",
shan: "山单善陕闪衫擅汕扇掺珊禅删膳缮赡鄯栅煽姗跚鳝嬗潸讪舢苫疝掸膻钐剡蟮芟埏彡骟",
zhan: "展战占站崭粘湛沾瞻颤詹斩盏辗绽毡栈蘸旃谵搌",
xin: "新心信辛欣薪馨鑫芯锌忻莘昕衅歆囟忄镡",
lian: "联连练廉炼脸莲恋链帘怜涟敛琏镰濂楝鲢殓潋裢裣臁奁莶蠊蔹",
chang: "场长厂常偿昌唱畅倡尝肠敞倘猖娼淌裳徜昶怅嫦菖鲳阊伥苌氅惝鬯",
zhang: "长张章障涨掌帐胀彰丈仗漳樟账杖璋嶂仉瘴蟑獐幛鄣嫜",
chao: "超朝潮炒钞抄巢吵剿绰嘲晁焯耖怊",
zhao: "着照招找召朝赵兆昭肇罩钊沼嘲爪诏濯啁棹笊",
zhou: "调州周洲舟骤轴昼宙粥皱肘咒帚胄绉纣妯啁诌繇碡籀酎荮",
che: "车彻撤尺扯澈掣坼砗屮",
ju: "车局据具举且居剧巨聚渠距句拒俱柜菊拘炬桔惧矩鞠驹锯踞咀瞿枸掬沮莒橘飓疽钜趄踽遽琚龃椐苣裾榘狙倨榉苴讵雎锔窭鞫犋屦醵",
cheng: "成程城承称盛抢乘诚呈净惩撑澄秤橙骋逞瞠丞晟铛埕塍蛏柽铖酲裎枨",
rong: "容荣融绒溶蓉熔戎榕茸冗嵘肜狨蝾",
sheng: "生声升胜盛乘圣剩牲甸省绳笙甥嵊晟渑眚",
deng: "等登邓灯澄凳瞪蹬噔磴嶝镫簦戥",
zhi: "制之治质职只志至指织支值知识直致执置止植纸拓智殖秩旨址滞氏枝芝脂帜汁肢挚稚酯掷峙炙栉侄芷窒咫吱趾痔蜘郅桎雉祉郦陟痣蛭帙枳踯徵胝栀贽祗豸鸷摭轵卮轾彘觯絷跖埴夂黹忮骘膣踬",
zheng: "政正证争整征郑丁症挣蒸睁铮筝拯峥怔诤狰徵钲",
tang: "堂唐糖汤塘躺趟倘棠烫淌膛搪镗傥螳溏帑羰樘醣螗耥铴瑭",
chi: "持吃池迟赤驰尺斥齿翅匙痴耻炽侈弛叱啻坻眙嗤墀哧茌豉敕笞饬踟蚩柢媸魑篪褫彳鸱螭瘛眵傺",
shi: "是时实事市十使世施式势视识师史示石食始士失适试什泽室似诗饰殖释驶氏硕逝湿蚀狮誓拾尸匙仕柿矢峙侍噬嗜栅拭嘘屎恃轼虱耆舐莳铈谥炻豕鲥饣螫酾筮埘弑礻蓍鲺贳",
qi: "企其起期气七器汽奇齐启旗棋妻弃揭枝歧欺骑契迄亟漆戚岂稽岐琦栖缉琪泣乞砌祁崎绮祺祈凄淇杞脐麒圻憩芪伎俟畦耆葺沏萋骐鳍綦讫蕲屺颀亓碛柒啐汔綮萁嘁蛴槭欹芑桤丌蜞",
chuai: "揣踹啜搋膪",
tuo: "托脱拓拖妥驼陀沱鸵驮唾椭坨佗砣跎庹柁橐乇铊沲酡鼍箨柝",
duo: "多度夺朵躲铎隋咄堕舵垛惰哆踱跺掇剁柁缍沲裰哚隳",
xue: "学血雪削薛穴靴谑噱鳕踅泶彐",
chong: "重种充冲涌崇虫宠忡憧舂茺铳艟",
chou: "筹抽绸酬愁丑臭仇畴稠瞅踌惆俦瘳雠帱",
qiu: "求球秋丘邱仇酋裘龟囚遒鳅虬蚯泅楸湫犰逑巯艽俅蝤赇鼽糗",
xiu: "修秀休宿袖绣臭朽锈羞嗅岫溴庥馐咻髹鸺貅",
chu: "出处础初助除储畜触楚厨雏矗橱锄滁躇怵绌搐刍蜍黜杵蹰亍樗憷楮",
tuan: "团揣湍疃抟彖",
zhui: "追坠缀揣椎锥赘惴隹骓缒",
chuan: "传川船穿串喘椽舛钏遄氚巛舡",
zhuan: "专转传赚砖撰篆馔啭颛",
yuan: "元员院原源远愿园援圆缘袁怨渊苑宛冤媛猿垣沅塬垸鸳辕鸢瑗圜爰芫鼋橼螈眢箢掾",
cuan: "窜攒篡蹿撺爨汆镩",
chuang: "创床窗闯幢疮怆",
zhuang: "装状庄壮撞妆幢桩奘僮戆",
chui: "吹垂锤炊椎陲槌捶棰",
chun: "春纯醇淳唇椿蠢鹑朐莼肫蝽",
zhun: "准屯淳谆肫窀",
cu: "促趋趣粗簇醋卒蹴猝蹙蔟殂徂",
dun: "吨顿盾敦蹲墩囤沌钝炖盹遁趸砘礅",
qu: "区去取曲趋渠趣驱屈躯衢娶祛瞿岖龋觑朐蛐癯蛆苣阒诎劬蕖蘧氍黢蠼璩麴鸲磲",
xu: "需许续须序徐休蓄畜虚吁绪叙旭邪恤墟栩絮圩婿戌胥嘘浒煦酗诩朐盱蓿溆洫顼勖糈砉醑",
chuo: "辍绰戳淖啜龊踔辶",
zu: "组族足祖租阻卒俎诅镞菹",
ji: "济机其技基记计系期际及集级几给积极己纪即继击既激绩急奇吉季齐疾迹鸡剂辑籍寄挤圾冀亟寂暨脊跻肌稽忌饥祭缉棘矶汲畸姬藉瘠骥羁妓讥稷蓟悸嫉岌叽伎鲫诘楫荠戟箕霁嵇觊麂畿玑笈犄芨唧屐髻戢佶偈笄跽蒺乩咭赍嵴虮掎齑殛鲚剞洎丌墼蕺彐芰哜",
cong: "从丛匆聪葱囱琮淙枞骢苁璁",
zong: "总从综宗纵踪棕粽鬃偬枞腙",
cou: "凑辏腠楱",
cui: "衰催崔脆翠萃粹摧璀瘁悴淬啐隹毳榱",
wei: "为位委未维卫围违威伟危味微唯谓伪慰尾魏韦胃畏帷喂巍萎蔚纬潍尉渭惟薇苇炜圩娓诿玮崴桅偎逶倭猥囗葳隗痿猬涠嵬韪煨艉隹帏闱洧沩隈鲔軎",
cun: "村存寸忖皴",
zuo: "作做座左坐昨佐琢撮祚柞唑嘬酢怍笮阼胙",
zuan: "钻纂攥缵躜",
da: "大达打答搭沓瘩惮嗒哒耷鞑靼褡笪怛妲",
dai: "大代带待贷毒戴袋歹呆隶逮岱傣棣怠殆黛甙埭诒绐玳呔迨",
tai: "大台太态泰抬胎汰钛苔薹肽跆邰鲐酞骀炱",
ta: "他它她拓塔踏塌榻沓漯獭嗒挞蹋趿遢铊鳎溻闼",
dan: "但单石担丹胆旦弹蛋淡诞氮郸耽殚惮儋眈疸澹掸膻啖箪聃萏瘅赕",
lu: "路六陆录绿露鲁卢炉鹿禄赂芦庐碌麓颅泸卤潞鹭辘虏璐漉噜戮鲈掳橹轳逯渌蓼撸鸬栌氇胪镥簏舻辂垆",
tan: "谈探坦摊弹炭坛滩贪叹谭潭碳毯瘫檀痰袒坍覃忐昙郯澹钽锬",
ren: "人任认仁忍韧刃纫饪妊荏稔壬仞轫亻衽",
jie: "家结解价界接节她届介阶街借杰洁截姐揭捷劫戒皆竭桔诫楷秸睫藉拮芥诘碣嗟颉蚧孑婕疖桀讦疥偈羯袷哜喈卩鲒骱",
yan: "研严验演言眼烟沿延盐炎燕岩宴艳颜殷彦掩淹阎衍铅雁咽厌焰堰砚唁焉晏檐蜒奄俨腌妍谚兖筵焱偃闫嫣鄢湮赝胭琰滟阉魇酽郾恹崦芫剡鼹菸餍埏谳讠厣罨",
dang: "当党档荡挡宕砀铛裆凼菪谠",
tao: "套讨跳陶涛逃桃萄淘掏滔韬叨洮啕绦饕鼗",
tiao: "条调挑跳迢眺苕窕笤佻啁粜髫铫祧龆蜩鲦",
te: "特忑忒铽慝",
de: "的地得德底锝",
dei: "得",
di: "的地第提低底抵弟迪递帝敌堤蒂缔滴涤翟娣笛棣荻谛狄邸嘀砥坻诋嫡镝碲骶氐柢籴羝睇觌",
ti: "体提题弟替梯踢惕剔蹄棣啼屉剃涕锑倜悌逖嚏荑醍绨鹈缇裼",
tui: "推退弟腿褪颓蜕忒煺",
you: "有由又优游油友右邮尤忧幼犹诱悠幽佑釉柚铀鱿囿酉攸黝莠猷蝣疣呦蚴莸莜铕宥繇卣牖鼬尢蚰侑",
dian: "电点店典奠甸碘淀殿垫颠滇癫巅惦掂癜玷佃踮靛钿簟坫阽",
tian: "天田添填甜甸恬腆佃舔钿阗忝殄畋栝掭",
zhu: "主术住注助属逐宁著筑驻朱珠祝猪诸柱竹铸株瞩嘱贮煮烛苎褚蛛拄铢洙竺蛀渚伫杼侏澍诛茱箸炷躅翥潴邾槠舳橥丶瘃麈疰",
nian: "年念酿辗碾廿捻撵拈蔫鲶埝鲇辇黏",
diao: "调掉雕吊钓刁貂凋碉鲷叼铫铞",
yao: "要么约药邀摇耀腰遥姚窑瑶咬尧钥谣肴夭侥吆疟妖幺杳舀窕窈曜鹞爻繇徭轺铫鳐崾珧",
die: "跌叠蝶迭碟爹谍牒耋佚喋堞瓞鲽垤揲蹀",
she: "设社摄涉射折舍蛇拾舌奢慑赦赊佘麝歙畲厍猞揲滠",
ye: "业也夜叶射野液冶喝页爷耶邪咽椰烨掖拽曳晔谒腋噎揶靥邺铘揲",
xie: "些解协写血叶谢械鞋胁斜携懈契卸谐泄蟹邪歇泻屑挟燮榭蝎撷偕亵楔颉缬邂鲑瀣勰榍薤绁渫廨獬躞",
zhe: "这者着著浙折哲蔗遮辙辄柘锗褶蜇蛰鹧谪赭摺乇磔螫",
ding: "定订顶丁鼎盯钉锭叮仃铤町酊啶碇腚疔玎耵",
diu: "丢铥",
ting: "听庭停厅廷挺亭艇婷汀铤烃霆町蜓葶梃莛",
dong: "动东董冬洞懂冻栋侗咚峒氡恫胴硐垌鸫岽胨",
tong: "同通统童痛铜桶桐筒彤侗佟潼捅酮砼瞳恸峒仝嗵僮垌茼",
zhong: "中重种众终钟忠仲衷肿踵冢盅蚣忪锺舯螽夂",
dou: "都斗读豆抖兜陡逗窦渎蚪痘蔸钭篼",
du: "度都独督读毒渡杜堵赌睹肚镀渎笃竺嘟犊妒牍蠹椟黩芏髑",
duan: "断段短端锻缎煅椴簖",
dui: "对队追敦兑堆碓镦怼憝",
rui: "瑞兑锐睿芮蕊蕤蚋枘",
yue: "月说约越乐跃兑阅岳粤悦曰钥栎钺樾瀹龠哕刖",
tun: "吞屯囤褪豚臀饨暾氽",
hui: "会回挥汇惠辉恢徽绘毁慧灰贿卉悔秽溃荟晖彗讳诲珲堕诙蕙晦睢麾烩茴喙桧蛔洄浍虺恚蟪咴隳缋哕",
wu: "务物无五武午吴舞伍污乌误亡恶屋晤悟吾雾芜梧勿巫侮坞毋诬呜钨邬捂鹜兀婺妩於戊鹉浯蜈唔骛仵焐芴鋈庑鼯牾怃圬忤痦迕杌寤阢",
ya: "亚压雅牙押鸭呀轧涯崖邪芽哑讶鸦娅衙丫蚜碣垭伢氩桠琊揠吖睚痖疋迓岈砑",
he: "和合河何核盖贺喝赫荷盒鹤吓呵苛禾菏壑褐涸阂阖劾诃颌嗬貉曷翮纥盍",
wo: "我握窝沃卧挝涡斡渥幄蜗喔倭莴龌肟硪",
en: "恩摁蒽",
n: "嗯唔",
er: "而二尔儿耳迩饵洱贰铒珥佴鸸鲕",
fa: "发法罚乏伐阀筏砝垡珐",
quan: "全权券泉圈拳劝犬铨痊诠荃醛蜷颧绻犭筌鬈悛辁畎",
fei: "费非飞肥废菲肺啡沸匪斐蜚妃诽扉翡霏吠绯腓痱芾淝悱狒榧砩鲱篚镄",
pei: "配培坏赔佩陪沛裴胚妃霈淠旆帔呸醅辔锫",
ping: "平评凭瓶冯屏萍苹乒坪枰娉俜鲆",
fo: "佛",
hu: "和护许户核湖互乎呼胡戏忽虎沪糊壶葫狐蝴弧瑚浒鹄琥扈唬滹惚祜囫斛笏芴醐猢怙唿戽槲觳煳鹕冱瓠虍岵鹱烀轷",
ga: "夹咖嘎尬噶旮伽尕钆尜",
ge: "个合各革格歌哥盖隔割阁戈葛鸽搁胳舸疙铬骼蛤咯圪镉颌仡硌嗝鬲膈纥袼搿塥哿虼",
ha: "哈蛤铪",
xia: "下夏峡厦辖霞夹虾狭吓侠暇遐瞎匣瑕唬呷黠硖罅狎瘕柙",
gai: "改该盖概溉钙丐芥赅垓陔戤",
hai: "海还害孩亥咳骸骇氦嗨胲醢",
gan: "干感赶敢甘肝杆赣乾柑尴竿秆橄矸淦苷擀酐绀泔坩旰疳澉",
gang: "港钢刚岗纲冈杠缸扛肛罡戆筻",
jiang: "将强江港奖讲降疆蒋姜浆匠酱僵桨绛缰犟豇礓洚茳糨耩",
hang: "行航杭巷夯吭桁沆绗颃",
gong: "工公共供功红贡攻宫巩龚恭拱躬弓汞蚣珙觥肱廾",
hong: "红宏洪轰虹鸿弘哄烘泓訇蕻闳讧荭黉薨",
guang: "广光逛潢犷胱咣桄",
qiong: "穷琼穹邛茕筇跫蛩銎",
gao: "高告搞稿膏糕镐皋羔锆杲郜睾诰藁篙缟槁槔",
hao: "好号毫豪耗浩郝皓昊皋蒿壕灏嚎濠蚝貉颢嗥薅嚆",
li: "理力利立里李历例离励礼丽黎璃厉厘粒莉梨隶栗荔沥犁漓哩狸藜罹篱鲤砺吏澧俐骊溧砾莅锂笠蠡蛎痢雳俪傈醴栎郦俚枥喱逦娌鹂戾砬唳坜疠蜊黧猁鬲粝蓠呖跞疬缡鲡鳢嫠詈悝苈篥轹",
jia: "家加价假佳架甲嘉贾驾嫁夹稼钾挟拮迦伽颊浃枷戛荚痂颉镓笳珈岬胛袈郏葭袷瘕铗跏蛱恝哿",
luo: "落罗络洛逻螺锣骆萝裸漯烙摞骡咯箩珞捋荦硌雒椤镙跞瘰泺脶猡倮蠃",
ke: "可科克客刻课颗渴壳柯棵呵坷恪苛咳磕珂稞瞌溘轲窠嗑疴蝌岢铪颏髁蚵缂氪骒钶锞",
qia: "卡恰洽掐髂袷咭葜",
gei: "给",
gen: "根跟亘艮哏茛",
hen: "很狠恨痕哏",
gou: "构购够句沟狗钩拘勾苟垢枸篝佝媾诟岣彀缑笱鞲觏遘",
kou: "口扣寇叩抠佝蔻芤眍筘",
gu: "股古顾故固鼓骨估谷贾姑孤雇辜菇沽咕呱锢钴箍汩梏痼崮轱鸪牯蛊诂毂鹘菰罟嘏臌觚瞽蛄酤牿鲴",
pai: "牌排派拍迫徘湃俳哌蒎",
gua: "括挂瓜刮寡卦呱褂剐胍诖鸹栝呙",
tou: "投头透偷愉骰亠",
guai: "怪拐乖",
kuai: "会快块筷脍蒯侩浍郐蒉狯哙",
guan: "关管观馆官贯冠惯灌罐莞纶棺斡矜倌鹳鳏盥掼涫",
wan: "万完晚湾玩碗顽挽弯蔓丸莞皖宛婉腕蜿惋烷琬畹豌剜纨绾脘菀芄箢",
ne: "呢哪呐讷疒",
gui: "规贵归轨桂柜圭鬼硅瑰跪龟匮闺诡癸鳜桧皈鲑刽晷傀眭妫炅庋簋刿宄匦",
jun: "军均俊君峻菌竣钧骏龟浚隽郡筠皲麇捃",
jiong: "窘炯迥炅冂扃",
jue: "决绝角觉掘崛诀獗抉爵嚼倔厥蕨攫珏矍蹶谲镢鳜噱桷噘撅橛孓觖劂爝",
gun: "滚棍辊衮磙鲧绲丨",
hun: "婚混魂浑昏棍珲荤馄诨溷阍",
guo: "国过果郭锅裹帼涡椁囗蝈虢聒埚掴猓崞蜾呙馘",
hei: "黑嘿嗨",
kan: "看刊勘堪坎砍侃嵌槛瞰阚龛戡凵莰",
heng: "衡横恒亨哼珩桁蘅",
mo: "万没么模末冒莫摩墨默磨摸漠脉膜魔沫陌抹寞蘑摹蓦馍茉嘿谟秣蟆貉嫫镆殁耱嬷麽瘼貊貘",
peng: "鹏朋彭膨蓬碰苹棚捧亨烹篷澎抨硼怦砰嘭蟛堋",
hou: "后候厚侯猴喉吼逅篌糇骺後鲎瘊堠",
hua: "化华划话花画滑哗豁骅桦猾铧砉",
huai: "怀坏淮徊槐踝",
huan: "还环换欢患缓唤焕幻痪桓寰涣宦垸洹浣豢奂郇圜獾鲩鬟萑逭漶锾缳擐",
xun: "讯训迅孙寻询循旬巡汛勋逊熏徇浚殉驯鲟薰荀浔洵峋埙巽郇醺恂荨窨蕈曛獯",
huang: "黄荒煌皇凰慌晃潢谎惶簧璜恍幌湟蝗磺隍徨遑肓篁鳇蟥癀",
nai: "能乃奶耐奈鼐萘氖柰佴艿",
luan: "乱卵滦峦鸾栾銮挛孪脔娈",
qie: "切且契窃茄砌锲怯伽惬妾趄挈郄箧慊",
jian: "建间件见坚检健监减简艰践兼鉴键渐柬剑尖肩舰荐箭浅剪俭碱茧奸歼拣捡煎贱溅槛涧堑笺谏饯锏缄睑謇蹇腱菅翦戬毽笕犍硷鞯牮枧湔鲣囝裥踺搛缣鹣蒹谫僭戋趼楗",
nan: "南难男楠喃囡赧腩囝蝻",
qian: "前千钱签潜迁欠纤牵浅遣谦乾铅歉黔谴嵌倩钳茜虔堑钎骞阡掮钤扦芊犍荨仟芡悭缱佥愆褰凵肷岍搴箝慊椠",
qiang: "强抢疆墙枪腔锵呛羌蔷襁羟跄樯戕嫱戗炝镪锖蜣",
xiang: "向项相想乡象响香降像享箱羊祥湘详橡巷翔襄厢镶飨饷缃骧芗庠鲞葙蟓",
jiao: "教交较校角觉叫脚缴胶轿郊焦骄浇椒礁佼蕉娇矫搅绞酵剿嚼饺窖跤蛟侥狡姣皎茭峤铰醮鲛湫徼鹪僬噍艽挢敫",
zhuo: "着著缴桌卓捉琢灼浊酌拙茁涿镯淖啄濯焯倬擢斫棹诼浞禚",
qiao: "桥乔侨巧悄敲俏壳雀瞧翘窍峭锹撬荞跷樵憔鞘橇峤诮谯愀鞒硗劁缲",
xiao: "小效销消校晓笑肖削孝萧俏潇硝宵啸嚣霄淆哮筱逍姣箫骁枭哓绡蛸崤枵魈",
si: "司四思斯食私死似丝饲寺肆撕泗伺嗣祀厮驷嘶锶俟巳蛳咝耜笥纟糸鸶缌澌姒汜厶兕",
kai: "开凯慨岂楷恺揩锴铠忾垲剀锎蒈",
jin: "进金今近仅紧尽津斤禁锦劲晋谨筋巾浸襟靳瑾烬缙钅矜觐堇馑荩噤廑妗槿赆衿卺",
qin: "亲勤侵秦钦琴禽芹沁寝擒覃噙矜嗪揿溱芩衾廑锓吣檎螓",
jing: "经京精境竞景警竟井惊径静劲敬净镜睛晶颈荆兢靖泾憬鲸茎腈菁胫阱旌粳靓痉箐儆迳婧肼刭弪獍",
ying: "应营影英景迎映硬盈赢颖婴鹰荧莹樱瑛蝇萦莺颍膺缨瀛楹罂荥萤鹦滢蓥郢茔嘤璎嬴瘿媵撄潆",
jiu: "就究九酒久救旧纠舅灸疚揪咎韭玖臼柩赳鸠鹫厩啾阄桕僦鬏",
zui: "最罪嘴醉咀蕞觜",
juan: "卷捐圈眷娟倦绢隽镌涓鹃鄄蠲狷锩桊",
suan: "算酸蒜狻",
yun: "员运云允孕蕴韵酝耘晕匀芸陨纭郧筠恽韫郓氲殒愠昀菀狁",
qun: "群裙逡麇",
ka: "卡喀咖咔咯佧胩",
kang: "康抗扛慷炕亢糠伉钪闶",
keng: "坑铿吭",
kao: "考靠烤拷铐栲尻犒",
ken: "肯垦恳啃龈裉",
yin: "因引银印音饮阴隐姻殷淫尹荫吟瘾寅茵圻垠鄞湮蚓氤胤龈窨喑铟洇狺夤廴吲霪茚堙",
kong: "空控孔恐倥崆箜",
ku: "苦库哭酷裤枯窟挎骷堀绔刳喾",
kua: "跨夸垮挎胯侉",
kui: "亏奎愧魁馈溃匮葵窥盔逵睽馗聩喟夔篑岿喹揆隗傀暌跬蒉愦悝蝰",
kuan: "款宽髋",
kuang: "况矿框狂旷眶匡筐邝圹哐贶夼诳诓纩",
que: "确却缺雀鹊阙瘸榷炔阕悫",
kun: "困昆坤捆琨锟鲲醌髡悃阃",
kuo: "扩括阔廓蛞",
la: "拉落垃腊啦辣蜡喇剌旯砬邋瘌",
lai: "来莱赖睐徕籁涞赉濑癞崃疠铼",
lan: "兰览蓝篮栏岚烂滥缆揽澜拦懒榄斓婪阑褴罱啉谰镧漤",
lin: "林临邻赁琳磷淋麟霖鳞凛拎遴蔺吝粼嶙躏廪檩啉辚膦瞵懔",
lang: "浪朗郎廊狼琅榔螂阆锒莨啷蒗稂",
liang: "量两粮良辆亮梁凉谅粱晾靓踉莨椋魉墚",
lao: "老劳落络牢捞涝烙姥佬崂唠酪潦痨醪铑铹栳耢",
mu: "目模木亩幕母牧莫穆姆墓慕牟牡募睦缪沐暮拇姥钼苜仫毪坶",
le: "了乐勒肋叻鳓嘞仂泐",
lei: "类累雷勒泪蕾垒磊擂镭肋羸耒儡嫘缧酹嘞诔檑",
sui: "随岁虽碎尿隧遂髓穗绥隋邃睢祟濉燧谇眭荽",
lie: "列烈劣裂猎冽咧趔洌鬣埒捩躐",
leng: "冷愣棱楞塄",
ling: "领令另零灵龄陵岭凌玲铃菱棱伶羚苓聆翎泠瓴囹绫呤棂蛉酃鲮柃",
lia: "俩",
liao: "了料疗辽廖聊寥缪僚燎缭撂撩嘹潦镣寮蓼獠钌尥鹩",
liu: "流刘六留柳瘤硫溜碌浏榴琉馏遛鎏骝绺镏旒熘鹨锍",
lun: "论轮伦仑纶沦抡囵",
lv: "率律旅绿虑履吕铝屡氯缕滤侣驴榈闾偻褛捋膂稆",
lou: "楼露漏陋娄搂篓喽镂偻瘘髅耧蝼嵝蒌",
mao: "贸毛矛冒貌茂茅帽猫髦锚懋袤牦卯铆耄峁瑁蟊茆蝥旄泖昴瞀",
long: "龙隆弄垄笼拢聋陇胧珑窿茏咙砻垅泷栊癃",
nong: "农浓弄脓侬哝",
shuang: "双爽霜孀泷",
shu: "术书数属树输束述署朱熟殊蔬舒疏鼠淑叔暑枢墅俞曙抒竖蜀薯梳戍恕孰沭赎庶漱塾倏澍纾姝菽黍腧秫毹殳疋摅",
shuai: "率衰帅摔甩蟀",
lve: "略掠锊",
ma: "么马吗摩麻码妈玛嘛骂抹蚂唛蟆犸杩",
me: "么麽",
mai: "买卖麦迈脉埋霾荬劢",
man: "满慢曼漫埋蔓瞒蛮鳗馒幔谩螨熳缦镘颟墁鞔",
mi: "米密秘迷弥蜜谜觅靡泌眯麋猕谧咪糜宓汨醚嘧弭脒冖幂祢縻蘼芈糸敉",
men: "们门闷瞒汶扪焖懑鞔钔",
mang: "忙盲茫芒氓莽蟒邙硭漭",
meng: "蒙盟梦猛孟萌氓朦锰檬勐懵蟒蜢虻黾蠓艨甍艋瞢礞",
miao: "苗秒妙描庙瞄缪渺淼藐缈邈鹋杪眇喵",
mou: "某谋牟缪眸哞鍪蛑侔厶",
miu: "缪谬",
mei: "美没每煤梅媒枚妹眉魅霉昧媚玫酶镁湄寐莓袂楣糜嵋镅浼猸鹛",
wen: "文问闻稳温纹吻蚊雯紊瘟汶韫刎璺玟阌",
mie: "灭蔑篾乜咩蠛",
ming: "明名命鸣铭冥茗溟酩瞑螟暝",
na: "内南那纳拿哪娜钠呐捺衲镎肭",
nei: "内那哪馁",
nuo: "难诺挪娜糯懦傩喏搦锘",
ruo: "若弱偌箬",
nang: "囊馕囔曩攮",
nao: "脑闹恼挠瑙淖孬垴铙桡呶硇猱蛲",
ni: "你尼呢泥疑拟逆倪妮腻匿霓溺旎昵坭铌鲵伲怩睨猊",
nen: "嫩恁",
neng: "能",
nin: "您恁",
niao: "鸟尿溺袅脲茑嬲",
nie: "摄聂捏涅镍孽捻蘖啮蹑嗫臬镊颞乜陧",
niang: "娘酿",
ning: "宁凝拧泞柠咛狞佞聍甯",
nu: "努怒奴弩驽帑孥胬",
nv: "女钕衄恧",
ru: "入如女乳儒辱汝茹褥孺濡蠕嚅缛溽铷洳薷襦颥蓐",
nuan: "暖",
nve: "虐疟",
re: "热若惹喏",
ou: "区欧偶殴呕禺藕讴鸥瓯沤耦怄",
pao: "跑炮泡抛刨袍咆疱庖狍匏脬",
pou: "剖掊裒",
pen: "喷盆湓",
pie: "瞥撇苤氕丿",
pin: "品贫聘频拼拚颦姘嫔榀牝",
se: "色塞瑟涩啬穑铯槭",
qing: "情青清请亲轻庆倾顷卿晴氢擎氰罄磬蜻箐鲭綮苘黥圊檠謦",
zan: "赞暂攒堑昝簪糌瓒錾趱拶",
shao: "少绍召烧稍邵哨韶捎勺梢鞘芍苕劭艄筲杓潲",
sao: "扫骚嫂梢缫搔瘙臊埽缲鳋",
sha: "沙厦杀纱砂啥莎刹杉傻煞鲨霎嗄痧裟挲铩唼歃",
xuan: "县选宣券旋悬轩喧玄绚渲璇炫萱癣漩眩暄煊铉楦泫谖痃碹揎镟儇",
ran: "然染燃冉苒髯蚺",
rang: "让壤攘嚷瓤穰禳",
rao: "绕扰饶娆桡荛",
reng: "仍扔",
ri: "日",
rou: "肉柔揉糅鞣蹂",
ruan: "软阮朊",
run: "润闰",
sa: "萨洒撒飒卅仨脎",
suo: "所些索缩锁莎梭琐嗦唆唢娑蓑羧挲桫嗍睃",
sai: "思赛塞腮噻鳃",
shui: "说水税谁睡氵",
sang: "桑丧嗓搡颡磉",
sen: "森",
seng: "僧",
shai: "筛晒",
shang: "上商尚伤赏汤裳墒晌垧觞殇熵绱",
xing: "行省星腥猩惺兴刑型形邢饧醒幸杏性姓陉荇荥擤悻硎",
shou: "收手受首售授守寿瘦兽狩绶艏扌",
shuo: "说数硕烁朔铄妁槊蒴搠",
su: "速素苏诉缩塑肃俗宿粟溯酥夙愫簌稣僳谡涑蔌嗉觫",
shua: "刷耍唰",
shuan: "栓拴涮闩",
shun: "顺瞬舜吮",
song: "送松宋讼颂耸诵嵩淞怂悚崧凇忪竦菘",
sou: "艘搜擞嗽嗖叟馊薮飕嗾溲锼螋瞍",
sun: "损孙笋荪榫隼狲飧",
teng: "腾疼藤滕誊",
tie: "铁贴帖餮萜",
tu: "土突图途徒涂吐屠兔秃凸荼钍菟堍酴",
wai: "外歪崴",
wang: "王望往网忘亡旺汪枉妄惘罔辋魍",
weng: "翁嗡瓮蓊蕹",
zhua: "抓挝爪",
yang: "样养央阳洋扬杨羊详氧仰秧痒漾疡泱殃恙鸯徉佯怏炀烊鞅蛘",
xiong: "雄兄熊胸凶匈汹芎",
yo: "哟唷",
yong: "用永拥勇涌泳庸俑踊佣咏雍甬镛臃邕蛹恿慵壅痈鳙墉饔喁",
za: "杂扎咱砸咋匝咂拶",
zai: "在再灾载栽仔宰哉崽甾",
zao: "造早遭枣噪灶燥糟凿躁藻皂澡蚤唣",
zei: "贼",
zen: "怎谮",
zeng: "增曾综赠憎锃甑罾缯",
zhei: "这",
zou: "走邹奏揍诹驺陬楱鄹鲰",
zhuai: "转拽",
zun: "尊遵鳟樽撙",
dia: "嗲",
nou: "耨",
};
const m = new Map();
Object.keys(data).forEach((key: string) => {
const str = data[key] as string;
const arr = str.split("");
arr.forEach((ch) => {
const arr = m.get(ch) || [];
arr.push(key);
m.set(ch, arr);
});
});
return m;
}
const map = newPinYinMap();
export default function getPinYin(str: string): string[] {
const pinYinArr: string[][] = [];
const size = str.length;
let pinYinSize = 1;
for (let i = 0; i < size; i++) {
const ch = str[i];
let value = map.get(ch);
if (!value || value.length === 0) {
value = [ch];
}
pinYinSize *= value.length;
pinYinArr.push(value);
}
const pinYin: string[] = [];
const firstPinYin: string[] = [];
for (let i = 0; i < pinYinSize; i++) {
pinYinArr.forEach((values) => {
const valueSize = values.length;
values.forEach((value, index) => {
if (i % valueSize === index) {
pinYin[i] = (pinYin[i] || "") + value;
firstPinYin[i] = (firstPinYin[i] || "") + value[0];
}
});
});
}
return [pinYin.join(""), firstPinYin.join("")];
}
================================================
FILE: src/helpers/util.ts
================================================
import { FormRules, MessageApi } from "naive-ui";
import dayjs from "dayjs";
import { get, has, isNil } from "lodash-es";
import { platform } from "@tauri-apps/api/os";
import { appWindow } from "@tauri-apps/api/window";
import { readText, writeText } from "@tauri-apps/api/clipboard";
import { relaunch } from "@tauri-apps/api/process";
import { BaseDirectory, writeBinaryFile, exists } from "@tauri-apps/api/fs";
import Debug from "debug";
import { appName } from "../constants/common";
import getPinYin from "./pinyin";
const debug = Debug("util");
export function isWebMode() {
return !window.__TAURI_IPC__;
}
export async function setAppTitle(title: string) {
if (isWebMode()) {
return;
}
if (title !== appName) {
title = `${appName} - ${title}`;
}
await appWindow.setTitle(title);
}
export function formatError(err: Error | unknown): string {
let message = "";
if (err instanceof Error) {
message = err.message;
} else if (has(err, "message")) {
message = get(err, "message");
} else {
message = err as string;
}
return message;
}
export function showError(message: MessageApi, err: Error | unknown): void {
message.error(formatError(err), {
duration: 3000,
});
}
// formatDate 格式化日期
export function formatDate(str: string): string {
if (!str) {
return "--";
}
return dayjs(str).format("YYYY-MM-DD HH:mm:ss");
}
export function formatSimpleDate(str: string): string {
if (!str) {
return "--";
}
const now = dayjs();
const date = dayjs(str);
if (date.year() === now.year()) {
return date.format("MM-DD HH:mm");
}
return date.format("YYYY-MM-DD");
}
export function getBodyWidth(): number {
return window.innerWidth || 800;
}
export function getNormalDialogStyle(percent = 0.7) {
const bodyWidth = getBodyWidth();
const modalWidth = bodyWidth >= 1000 ? bodyWidth * percent : bodyWidth - 200;
const modalStyle = {
width: `${modalWidth}px`,
};
return modalStyle;
}
export function newRequireRules(keys: string[]) {
const rules: FormRules = {};
keys.map((key) => {
rules[key] = {
required: true,
trigger: "blur",
};
});
return rules;
}
export function tryToParseArray(data: string) {
if (!data) {
return [];
}
const body = data.trim();
if (body.length <= 2 || body[0] !== "[" || body[body.length - 1] !== "]") {
return [];
}
return JSON.parse(body);
}
export async function writeTextToClipboard(text: string) {
if (isWebMode()) {
navigator.clipboard.writeText(text);
return;
}
await writeText(text);
}
export async function readTextFromClipboard() {
if (isWebMode()) {
return navigator.clipboard.readText();
}
return readText();
}
export async function reload() {
if (isWebMode()) {
window.location.reload();
} else {
relaunch();
}
}
export async function delay(ms: number) {
await new Promise((resolve) => setTimeout(resolve, ms));
}
export function formatLatency(ms: number) {
if (isNil(ms)) {
return "--";
}
if (ms < 1000) {
return `${ms.toLocaleString()} ms`;
}
return `${(ms / 1000).toFixed(2)} s`;
}
export function isJSON(data: string) {
if (!data || data.length < 2) {
return false;
}
const value = `${data[0]}${data[data.length - 1]}`;
return value === "[]" || value === "{}";
}
export function jsonFormat(data: string) {
try {
const result = JSON.stringify(JSON.parse(data), null, 2);
return result;
} catch (err) {
const arr = data.split("\n");
if (arr.length < 2) {
throw err;
}
// 如果第一次出错,判断是否有换行,如果有,则一行行parse
return arr
.map((item) => {
if (!isJSON(item)) {
return item;
}
return JSON.stringify(JSON.parse(item), null, 4);
})
.join("\n");
}
}
export function convertHTTPHeaderName(name: string) {
const arr = name.split("-");
return arr
.map((item) => `${item[0].toUpperCase()}${item.substring(1)}`)
.join("-");
}
function stringToArrayBuffer(data: string): Promise {
return new Promise((resolve) => {
const b = new Blob([data]);
const f = new FileReader();
f.onload = (e) => {
resolve(e.target?.result as ArrayBuffer);
};
f.readAsArrayBuffer(b);
});
}
export function isMatchTextOrPinYin(content: string, keyword: string) {
const k = keyword.toLowerCase();
if (content.toLowerCase().includes(k)) {
return true;
}
const arr = getPinYin(content);
debug("pinyin:%s", arr.join(","));
for (let i = 0; i < arr.length; i++) {
if (arr[i].toLowerCase().includes(k)) {
return true;
}
}
return false;
}
export async function writeFileToDownload(file: string, data: ArrayBuffer) {
const arr = file.split(".");
let baseFileName = arr[0];
let ext = "";
if (arr.length >= 2) {
baseFileName = arr.slice(0, arr.length - 1).join(".");
ext = `.${arr[arr.length - 1]}`;
}
const opt = {
dir: BaseDirectory.Download,
};
// 如果有重名的,则数字+1
for (let i = 0; i < 10; i++) {
const file = (i === 0 ? baseFileName : `${baseFileName}-${i}`) + ext;
const fileExists = await exists(file, opt);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (!fileExists) {
await writeBinaryFile(file, data, opt);
return;
}
}
throw new Error(`file(${file}) exist`);
}
export async function writeSettingToDownload(arr: unknown, name: string) {
const data = JSON.stringify(arr, null, 4);
const buf = await stringToArrayBuffer(data);
await writeFileToDownload(`cyberapi-${name}.json`, buf);
}
export async function isMacOS() {
if (isWebMode()) {
return Promise.resolve(false);
}
const platformName = await platform();
return platformName === "darwin";
}
================================================
FILE: src/i18n/en.ts
================================================
export default {
collection: {
filterPlaceholder: "filter keywords",
newHTTPRequest: "Create a HTTP request",
newFolder: "Create a folder",
deleteSetting: "Delete configurations",
deleteSettingContent: "Please confirm the deletion of configurations(%s)",
modifySetting: "Modify configuration",
copySetting: "Copy configuration",
copySettingSuccess:
"Configurations have been copied to clipboard successfully",
shouldSelectAPISettingFirst: "Please select API request first",
send: "Send",
abort: "Abort",
format: "Format",
changeContentType: "Change data type",
changeContentTypeContent:
"The original data will be cleared after changing the data type, so please confirm the change",
namePlaceholder: "name",
valuePlaceholder: "value",
requesting: "Requesting...",
copyAsCURL: "Copy as curl",
copyAsCURLSuccess: "CURL has been copied to the clipboard successfully",
pinRequest: "Pin request",
importSettings: "Import configurations",
importSuccess: "Imported configurations successfully",
exportSettings: "Export configurations",
dragUploadTips: "Click or drag a file to this area to upload",
exportSettingsSuccess:
"The configurations are successfully exported to the download directory",
curlTooLargeTips:
"The curl data is too large, and the content has been copied to the clipboard",
curlGenerateFail: "Generate curl fail",
apiID: "Request ID",
remoteAddr: "Remote Addr",
dns: "DNS",
tcp: "TCP",
tls: "TLS",
cipher: "Cipher Suite",
serverProcessing: "Server Processing",
contentTransfer: "Content Transfer",
},
common: {
app: "CyberAPI",
dashboard: "Dashboard",
add: "Add",
confirm: "Confirm",
back: "Return",
name: "Name",
nameRequireError: "name can not be blank",
namePlaceholder: "Please enter a name",
description: "Description",
descriptionPlaceholder: "Please enter description",
modify: "Modify",
loading: "Loading",
delete: "Delete",
duplicate: "Duplicate",
settings: "Configurations",
create: "Create",
keywordFilterPlaceholder: "Please enter a keyword",
selectFile: "Please select a file",
saveToDownloadSuccess: "File is save to download successful",
untitled: "Untitled",
},
dashboard: {
newCollection: "Create API group",
updateCollection: "Update API group",
deleteCollection: "Delete API Group",
deleteCollectionContent:
"Please confirm the deletion of API group, which can not be recovered after deletion!",
deleteCollectionDone: "The API group was successfully deleted",
sortLastModified: "Modify Time",
sortNameAsc: "Name Asc",
sortNameDesc: "Name Desc",
sortOlderFirst: "Older First",
sortNewestFirst: "Newest First",
},
setting: {
title: "Application Setting",
themeTitle: "Please select the color theme of application",
darkTheme: "Dark Theme",
lightTheme: "Light Theme",
systemTheme: "System Theme",
infoTitle: "Application Information",
appVersion: "Version",
platform: "Platform",
arch: "Architecture",
os: "OS",
userAgent: "User Agent",
dir: "Directory",
osVersion: "OS version",
windowSize: "Application Window",
windowWidth: "Width",
windowResizeType: "Size type of application window",
windowMaxSize: "Maximize",
windowCustomSize: "Customize",
windowWidthPlaceholder: "Please enter window width",
windowHeight: "Height",
windowHeightPlaceholder: "Please enter window height",
cookieSetting: "Cookie",
storeSetting: "Storage",
appSetting: "Setting",
envSetting: "ENV",
reqHeaderSetting: "Req Header",
exportTables: "Backup",
exportTablesProcessing: "Backup is processing, please wait...",
exportTablesSuccess: "Backup save in download folder: %s",
importTables: "Restore",
importTablesTips:
"Restore will remove the original data. Are you sure to restore?",
importTablesSuccess:
"Restore success, and the application will restart in 3 seconds",
customizeVariableSetting: "Variable",
langChangeSuccess:
"Language configuration is modified successfully, and the application will restart in 3 seconds",
browser: "Browser",
timeoutSetting: "Request Timeout",
timeoutConnect: "Connect Timeout(s)",
timeoutRead: "Read Timeout(s)",
timeoutWrite: "Write Timeout(s)",
},
cookie: {
title: "Cookie",
name: "Name",
namePlaceholder: "Please enter the name of cookie",
value: "Value",
valuePlaceholder: "Please enter the value of cookie",
path: "Path",
pathPlaceholder: "Please enter the path of cookie",
domain: "Domain",
domainPlaceholder: "Please enter the domain of cookie",
expires: "Expiry Date",
expiresPlaceholder: "Please enter the expiry date of cookie",
op: "Operation",
deleteCookie: "Delete cookie",
deleteCookieContent: "Please confirm the deletion of cookie",
neverExpired: "Never Expired",
clearCookieTips: "Are you sure to clear all cookie?",
clearCookie: "Clear Cookie",
},
environment: {
title: "Environment",
tips: "Use current environment to facilitate switching between different environments(only for current project)",
uriIsNil: "request address can not be blank",
addNew: "Add environmental variable",
clearCurrent: "Clear current value",
},
customizeVariable: {
title: "Customize Variable",
tips: "Customize commonly used variables, get using value function(only for current project)",
},
globalReqHeader: {
title: "HTTP Request Header",
tips: "Customize http request header, they will be appended to request(only for current project)",
},
store: {
title: "Storage",
name: "Storage",
desc: "Description",
op: "Operation",
settingStore: "Setting",
settingStoreDesc:
"Save configurations such as application window, theme and etc.",
pinRequestsStore: "Pin",
pinRequestsStoreDesc: "Save the relevant request information for topping",
latestResponseStore: "LatestRequest",
latestResponseStoreDesc: "Save the latest request response",
clearTips:
"Please confirm the clearing (%s) of all data, which can not be recovered after clearing",
clearSuccess:
"The stored data has been cleared, and the application will restart in 3 seconds",
responseList: "Response List",
noHistory: "There is no history response",
clearHistory: "Clear History",
clearHistorySuccess: "The history of request has been cleared",
},
};
================================================
FILE: src/i18n/index.ts
================================================
import { createI18n } from "vue-i18n";
import { enUS, zhCN, ukUA } from "naive-ui";
import en from "./en";
import zh from "./zh";
import uk from "./uk";
export enum LANG {
en = "en",
zh = "zh",
uk = "uk",
}
const i18n = createI18n({
locale: "en",
fallbackLocale: "en",
messages: {
en,
zh,
uk,
},
});
export default i18n;
export function getLocale() {
if (i18n.global.locale === LANG.zh) {
return zhCN;
}
if (i18n.global.locale === LANG.uk) {
return ukUA;
}
return enUS;
}
export function getCurrentLang() {
return i18n.global.locale;
}
export function changeI18nLocale(locale: string) {
if (locale === LANG.zh || locale === LANG.en || locale === LANG.uk) {
i18n.global.locale = locale;
}
}
export function i18nGet(
key: string,
named: Record = {},
): string {
return i18n.global.t(key, named);
}
export function newI18nGet(prefix: string) {
if (prefix[prefix.length - 1] !== ".") {
prefix += ".";
}
return function (key: string, named: Record = {}): string {
return i18n.global.t(prefix + key, named);
};
}
export const i18nCollection = newI18nGet("collection");
export const i18nCommon = newI18nGet("common");
export const i18nDashboard = newI18nGet("dashboard");
export const i18nSetting = newI18nGet("setting");
export const i18nCookie = newI18nGet("cookie");
export const i18nEnvironment = newI18nGet("environment");
export const i18nGlobalReqHeader = newI18nGet("globalReqHeader");
export const i18nStore = newI18nGet("store");
export const i18nCustomizeVariable = newI18nGet("customizeVariable");
================================================
FILE: src/i18n/uk.ts
================================================
export default {
collection: {
filterPlaceholder: "фільтрувати ключові слова",
newHTTPRequest: "Створити HTTP-запит",
newFolder: "Створити папку",
deleteSetting: "Видалити HTTP-запит",
deleteSettingContent: "Підтвердіть видалення (%s)",
modifySetting: "Налаштування",
copySetting: "Скопіювати HTTP-запит",
copySettingSuccess: "Успішно скопійовано до буферу обміну",
shouldSelectAPISettingFirst: "Спочатку виберіть запит до API",
send: "Відправити",
abort: "Перервати",
format: "Формат",
changeContentType: "Змінити тип даних",
changeContentTypeContent:
"Початкові дані будуть очищені після зміни типу даних, тому, будь ласка, підтвердіть зміну",
namePlaceholder: "ключ",
valuePlaceholder: "значення",
requesting: "Відправлення запиту...",
copyAsCURL: "Скопіювати як curl",
copyAsCURLSuccess: "CURL успішно скопійовано до буфера обміну",
pinRequest: "Закріпити запит",
importSettings: "Імпорт HTTP-запита",
importSuccess: "Імпортовано успішно",
exportSettings: "Експорт HTTP-запита",
dragUploadTips:
"Натисніть або перетягніть файл до цієї області, щоб завантажити його",
exportSettingsSuccess: "Успішно експортовано до каталогу для завантаження",
curlTooLargeTips:
"Дані curl занадто великі, тому вміст було скопійовано до буфера обміну",
curlGenerateFail: "Невдала спроба генерування curl",
apiID: "Ідентифікатор запиту",
remoteAddr: "Віддалена адреса",
dns: "DNS",
tcp: "TCP",
tls: "TLS",
cipher: "Cipher Suite",
serverProcessing: "Обробка на сервері",
contentTransfer: "Передача вмісту",
},
common: {
app: "CyberAPI UA",
dashboard: "Головна",
add: "Додати",
confirm: "Підтвердити",
back: "Повернутись",
name: "Назва",
nameRequireError: "назва не може бути порожньою",
namePlaceholder: "Введіть назву",
description: "Опис",
descriptionPlaceholder: "Ввведіть опис",
modify: "Редагувати",
loading: "Завантаження",
delete: "Видалити",
duplicate: "Дублікат",
settings: "Налаштування",
create: "Створити",
keywordFilterPlaceholder: "Введіть ключове слово",
selectFile: "Виберіть файл",
saveToDownloadSuccess: "Файл збережено для успішного завантаження",
untitled: "Без назви",
},
dashboard: {
newCollection: "Створити групу API",
updateCollection: "Оновити API групи",
deleteCollection: "Видалити групу API",
deleteCollectionContent:
"Будь ласка, підтвердіть видалення групи API. Увага! Група API НЕ МОЖЕ відновлена після видалення!",
deleteCollectionDone: "Групу API успішно видалено",
sortLastModified: "За датою зміни",
sortNameAsc: "За зростанням",
sortNameDesc: "За убуванням",
sortOlderFirst: "Старіший перший",
sortNewestFirst: "Найновіший перший",
},
setting: {
title: "Налаштування програми",
themeTitle: "Оберіть тему програми",
darkTheme: "Темна тема",
lightTheme: "Світла тема",
systemTheme: "Системна тема",
infoTitle: "Програмна інформація",
appVersion: "Версія",
platform: "Платформа",
arch: "Архітектура",
os: "ОС",
userAgent: "Користувацький агент",
dir: "Каталог установки",
osVersion: "Версія ОС",
windowSize: "Вікно програми",
windowWidth: "Ширина",
windowResizeType: "Тип розміру вікна програми",
windowMaxSize: "Розгорнути",
windowCustomSize: "Налаштування",
windowWidthPlaceholder: "Введіть ширину вікна",
windowHeight: "Висота",
windowHeightPlaceholder: "Введіть висоту вікна",
cookieSetting: "Кукі",
storeSetting: "Сховище",
appSetting: "Налаштування",
envSetting: "ENV",
reqHeaderSetting: "Заголовок запиту",
exportTables: "Резервна копія",
exportTablesProcessing:
"Резервна копія обробляється, будь ласка, зачекайте...",
exportTablesSuccess: "Збереження резервної копії в папці: %s",
importTables: "Відновити",
importTablesTips:
"Відновлення видалить оригінальні дані. Ви впевнені, що хочете відновити?",
importTablesSuccess:
"Відновлення успішно завершилося, програма перезапуститься через 3 секунди",
customizeVariableSetting: "Variable",
langChangeSuccess:
"Мовну конфігурацію успішно змінено, програма перезапуститься через 3 секунди",
browser: "Браузер",
timeoutSetting: "Тайм-аут HTTP запиту",
timeoutConnect: "Тайм-аут(и) з'єднання",
timeoutRead: "Зчитування Тайм-аута(ів)",
timeoutWrite: "Записування Тайм-аута(ів)",
},
cookie: {
title: "Кукі",
name: "Назва",
namePlaceholder: "Введіть назву файлу кукі",
value: "Значення",
valuePlaceholder: "Введіть значення файлу кукі",
path: "Шлях",
pathPlaceholder: "Введіть шлях до файлу кукі",
domain: "Домен",
domainPlaceholder: "Введіть домен до файлу кукі",
expires: "Термін дії",
expiresPlaceholder: "Введіть термін дії файлу кукі",
op: "Операції",
deleteCookie: "Видалити кукі",
deleteCookieContent: "Будь ласка, підтвердіть видалення файлу кукі",
neverExpired: "Термін дії ніколи не закінчується",
clearCookieTips: "Ви впевнені, що хочете видалити всі файли кукі?",
clearCookie: "Очистити файл кукі",
},
environment: {
title: "Навколишнє середовище",
tips: "Використовувати поточне середовище для полегшення перемикання між різними середовищами (тільки для поточного проекту)",
uriIsNil: "адреса HTTP-запиту не може бути порожньою",
addNew: "Додати змінну середовища",
clearCurrent: "Очистити поточне значення",
},
customizeVariable: {
title: "Налаштувати змінну",
tips: "Налаштування часто використовуваних змінних, отримання за допомогою функції value (тільки для поточного проекту)",
},
globalReqHeader: {
title: "Заголовок HTTP запиту",
tips: "Налаштуйте заголовок HTTP запиту, вони будуть додані до запиту (тільки для поточного проекту)",
},
store: {
title: "Сховище",
name: "Сховище1",
desc: "Опис",
op: "Операції",
settingStore: "Налаштування",
settingStoreDesc:
"Збереження налаштувань, таких як вікно програми, тема тощо.",
pinRequestsStore: "Закріпити",
pinRequestsStoreDesc:
"Збережіть відповідну інформацію про HTTP запит для додавання",
latestResponseStore: "Останній HTTP запит",
latestResponseStoreDesc: "Збережіть останню відповідь на HTTP запит",
clearTips:
"Будь ласка, підтвердіть очищення (%s) всіх даних. Увага! Данні НЕ МОЖУТЬ бути відновлені після очищення",
clearSuccess:
"Збережені дані було очищено, програма перезапуститься через 3 секунди",
responseList: "Список відповідей",
noHistory: "Історія порожня",
clearHistory: "Очистити історію",
clearHistorySuccess: "Історія HTTP запитів очищена",
},
};
================================================
FILE: src/i18n/zh.ts
================================================
export default {
collection: {
filterPlaceholder: "关键字过滤",
newHTTPRequest: "新建HTTP请求",
newFolder: "新建目录",
deleteSetting: "删除配置",
deleteSettingContent: "确认要删除配置(%s)吗?",
modifySetting: "修改配置",
copySetting: "复制配置",
copySettingSuccess: "配置已成功复制至粘贴板",
shouldSelectAPISettingFirst: "请先选择接口请求",
send: "发送",
abort: "中止",
format: "格式化",
changeContentType: "更换数据类型",
changeContentTypeContent:
"更换数据类型后原有的数据会清除,请确认是否要更换?",
namePlaceholder: "名称",
valuePlaceholder: "值",
requesting: "请求中...",
copyAsCURL: "复制为curl",
copyAsCURLSuccess: "curl已成功复制至粘贴板",
pinRequest: "置顶请求",
importSettings: "导入配置",
importSuccess: "已经成功导入配置",
exportSettings: "导出配置",
dragUploadTips: "点击或拖动文件至此区域上传",
importFromJSONFile: "导入CyberAPI的配置",
exportSettingsSuccess: "配置已成功导出至下载目录",
curlTooLargeTips: "curl数据太大,内容已复制至粘贴板",
curlGenerateFail: "curl生成失败",
apiID: "该请求配置ID",
remoteAddr: "请求地址",
dns: "域名解析",
tcp: "TCP握手",
tls: "TLS握手",
cipher: "密码套件",
serverProcessing: "服务器处理",
contentTransfer: "数据传输",
},
common: {
app: "CyberAPI",
dashboard: "面板",
add: "添加",
confirm: "确定",
back: "返回",
name: "名称",
nameRequireError: "名称不能为空",
namePlaceholder: "请输入名称",
description: "描述",
descriptionPlaceholder: "请输入描述内容",
modify: "修改",
loading: "加载中...",
delete: "删除",
duplicate: "复制",
settings: "设置",
create: "创建",
keywordFilterPlaceholder: "请输入关键字",
selectFile: "请选择文件",
saveToDownloadSuccess: "文件已成功保存至下载目录",
untitled: "未命名",
},
dashboard: {
newCollection: "创建API分组",
updateCollection: "更新API分组",
deleteCollection: "删除API分组",
deleteCollectionContent: "确认要删除API分组吗?删除后无法恢复!",
deleteCollectionDone: "已成功删除该API分组",
sortLastModified: "更新时间",
sortNameAsc: "名称升序",
sortNameDesc: "名称降序",
sortOlderFirst: "旧的在前",
sortNewestFirst: "新的在前",
},
setting: {
title: "应用设置",
themeTitle: "请选择应用的配色主题",
darkTheme: "深色主题",
lightTheme: "浅色主题",
systemTheme: "系统主题",
infoTitle: "应用信息",
appVersion: "版本",
platform: "平台",
arch: "架构",
os: "系统",
userAgent: "User Agent",
dir: "应用目录",
osVersion: "系统版本",
windowSize: "应用窗口设置",
windowWidth: "窗口宽度",
windowResizeType: "窗口大小类型",
windowMaxSize: "最大化",
windowCustomSize: "自定义",
windowWidthPlaceholder: "输入窗口宽度",
windowHeight: "窗口高度",
windowHeightPlaceholder: "输入窗口高度",
cookieSetting: "Cookie设置",
storeSetting: "存储设置",
appSetting: "应用设置",
envSetting: "环境变量设置",
reqHeaderSetting: "请求头设置",
exportTables: "数据备份",
exportTablesProcessing: "数据正在备份中,请稍候...",
exportTablesSuccess: "数据成功备份至下载目录:%s",
importTables: "数据导入",
importTablesTips: "导入数据会先清空原有数据,确认是否导入?",
importTablesSuccess: "数据已成功导入,应用将在3秒后重启",
customizeVariableSetting: "变量设置",
langChangeSuccess: "修改语言配置成功,应用将在3秒后重启",
browser: "浏览器",
timeoutSetting: "请求超时设置",
timeoutConnect: "连接超时(秒)",
timeoutRead: "读超时(秒)",
timeoutWrite: "写超时(秒)",
},
cookie: {
title: "Cookie设置",
name: "名称",
namePlaceholder: "请输入cookie的名称",
value: "值",
valuePlaceholder: "请输入cookie的值",
path: "路径",
pathPlaceholder: "请输入cookie的路径",
domain: "域名",
domainPlaceholder: "请输入cookie的域名",
expires: "有效期",
expiresPlaceholder: "请选择cookie有效期",
op: "操作",
deleteCookie: "删除Cookie",
deleteCookieContent: "确认要删除Cookie吗?",
neverExpired: "永久有效",
clearCookieTips: "请确认是否清除所有的Cookie?",
clearCookie: "清除Cookie",
},
environment: {
title: "环境变量设置",
tips: "使用当前各环境变量,方便不同环境的切换(仅用于当前项目)",
uriIsNil: "请求地址不能为空",
addNew: "添加环境变量",
clearCurrent: "清除当前值",
},
customizeVariable: {
title: "自定义变量",
tips: "自定义变量,可存储常用的变量值,通过value函数获取变量值(仅用于当前项目)",
},
globalReqHeader: {
title: "HTTP请求头",
tips: "自定义的HTTP请求头,用于全局添加至所有请求(仅用于当前项目)",
},
store: {
title: "应用相关存储",
name: "存储",
desc: "描述",
op: "操作",
settingStore: "应用配置",
settingStoreDesc: "保存应用窗口、主题等配置信息",
pinRequestsStore: "置顶配置",
pinRequestsStoreDesc: "保存置顶的相关请求信息",
latestResponseStore: "请求响应",
latestResponseStoreDesc: "保存最新的请求响应",
clearTips: "确认清除(%s)的所有数据吗?清除后无法恢复!",
clearSuccess: "存储数据已清除,应用将在3秒后重启",
noHistory: "该请求无历史响应记录",
responseList: "响应记录",
clearHistory: "清除历史响应",
clearHistorySuccess: "已成功清除历史响应",
},
};
================================================
FILE: src/icons.ts
================================================
export const openFolderIcon =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAIsUlEQVR4Xu2XTahkx3mGn7equvv+zUzmZmTPCCl/jhHCHuyQQBZeZxUMxmQpC5NFEpJFcBZeGNkQe+UsQ35sHLKQTUggOGDvvAskkEXsCRJChIxGUSRGyszozs+dubfPOVXfGw1yQ3PPSesOcbAWeqA5p84pqt+3+vu+/oqfKh/wAR/wAWIaWsRjmK/x0+WFnNNfsIHCNHR9nMX+XZ9wuRpves7EHI92bPR8dC/xPeDRDdj+1aHGR5ZdMEITahkpnXo+RpvnK7Fv+1PANUlvniqEPvPclU9/+0sf/5u97bLH+4R//Ncbr//p99/86ne/+sm/4gSJNT773I8+df5c/odaY4/3EX2LJ9/R9a3f+sqVZzaG0GLu3291yPw/MFTzvX++wQuvH4FZ471zxcClCzt8aJ+vf/nbr1/92uee/JdJA3UYfi4q2OYnzT+98DY/fOUOaLy+JGy/Z4pIerzV9rfAL0wa6LtBwrR49/OT4j+vH/GDK7cIB7ASCwKQgADABmlN9mguJMWTkyH0R3/5iu4/uL+3tzfn63//2kR9G41Pjd0AT5cje6UcACJWaqfnKvWTBlKqX9zZ0SccAzXYUOnHaFOVHK2zKfrXMZOkuDdpoLXu2dYCsY4x63gkbPIHOiFOePUMoRMSPd6mDTZk3WTaQORowaNhvBI1KcKMMafFE4ZlPZg0EC3utZGB08a9R3MfjU35ZtbxdA5ANB9E80iABDagtVfebBJtiIcNrch43ni9EAcTBv5cEXGntUBiVMJW4iRhjFg3NWL0btKzQRp7ZHPzR2u6CbcEfwf8gQv8sfiVzxPx0p2IAJiqydMxvmFzpxG2kcaGpRO1a/r7SdLAV16F738ertxS4de/AM9coR2Wu63F5pZz/d30/UjUJiRh+5ESJ6d0TNsWv33FfOcLFH7jv+D6eXn38GEInRAa77X4Kfb9NAI3J8QKIVqKmx979ZZeig/xjnYX5se6/GJVuxy3IwyAvV7jAxAS2EbS6goGY5is52aFJLAxQmvl0bA2ni7P672TBKnq3u5OpMtX78WLlwulvCGd2RsUzvdbC/B6+VpfXCAAgwECJGyviwRptJl2IAQymJW4Ua1fiRWa/H+RIBJxOBSd3xtU3phRPnrvkKM9qdV0J1rg/zVLxyV2NUDr7z1dAvGJdT2dLAbk1WBU0nPmsEtOR9l+R7vL+bP7ens4SDV8EBGEp3sDdKoyNGYVdpNzR+Y2ri1BX9utWZ6l49b7Z8/uu/S+q5bmiuZlhInwqRpRbT7MT1f1Rz0WnzCQBEr02Sn1aR4PtZc+UEHqm+/SgmbAPmXdPwVj05vZ9D0SKqlG61PJM/WBSuUMaod67YCDJ/fsVkO8X0miVu5KiFZV8xnK0N7SnIX6mDvCXYvYgg0x8ajPWGtD7NV1MiaFALA9uY5IHDzwbZElevXtLRV8DtUO76II32ktLm4+r46fS+BgIxKAcMT0WmFg3ShwIpqTRGe3mEnlgaCco9hbEEdyy7QUNyPi4no5sANJgMHgE4KA1c5uTnm8uk4kxfp/SGBAGvdjEWnZRYQNRJO9RZEOUS5WFra71rwSMrpu7oPjRJrG+J0DpE299YbzmMgp3razlXlXsw4pyh0tidwV5x29LvxrrRlP1BCNmoT1lmP6XLv57OvJo+oUOUHO+u/o7VlNbrMByZQ5Tzg4cNXgRvnG1rz95lDbPMIreePAWB+dqp8XXjtPrMBTibp+5gAJJDEryXb+ZlKOSu/Mtufsu5R0w8dsOzXi+rD44cV0+zOzwh9inkAkIIGSvbpHgIB0IoYEAo1PNeN7AIy0XnICMGCJwAQiwAG6VZ3/+s1+9wcpK9xm0fLMc264lIDq3oucI2rfrmv3R0K/A2lLYguzbdgGb0PaAi8Ec8MMyEAGJ9DKGMZiAq03OdigABrQBIOhB3UQS9Cx4BhxbLOEWBo3176lMo/aehdByWnH83YvhthRSoocVKeUbAksCTBhVIV70Nx4BhRDTpCMEiCDACAxhTEAAoMsiIAQNEMFDeDeqBN0iM7QGXVSGlJEbYkYgpjTRU5nXW4+CO/vFt9unefOLVJVtKSUARujQDT53cXBM+FsUhFOxj82YIEwBltMIazVHGRwCAUoRFRDAwahHnmw6cG9pD4aQ8qtlihtqc67ufih9nLtw3NfvFW9U1ock5l7C6UlYAs55IZVZRdLXTLFSVl28mr3kWSAJGHATGIBAsKWMFgQwmGlUNBCVNnVUJGHZFUTgxJV3qsdQ9uxY77Mfqi9cCm48fbC++letH7GkAYWs6C2ZELWzA1TlchEZEOGlCwSJskIjEEiwGw+Xmq1/zYAwhYBhImQaUpqmIZormpOriVH64ahzUOxPa9xQzvmUlC4dt5Xnzrg569ucWmnxf0usaxiMU9257BzNpFsJ6EknCSnaJZIspAtcAgbMhswNEDCSpaMjO1wytgQRhF2gELOIaKlWYpl79gSsbfT4s1+y689hR9qL3xT8GcX/Nov3UDXGo8vHLNWfRTFZEepy1BeqEFyTkrNqcpKSNQEatIi8Cr0nbBDTCBlUwJkpAadwNkUU4ULyZEVinBG4bp0zfMg5DOlxnYuvt7nd7Rm8zMXzHNGAHBd/J7h6Zd15t/3+OhB0dbM6raXOm5ZwxBKi4VS7eRAyoNyzPBiRrgqWgUEFE5HBUzKhaRidQMtDbjNrISjLBxd59kseTs3L463vBzk/9ivPnzqPrz8tPmGgMctVvCSePYX4fK/QULn35jzkZtWKlVbJRi80NIHDG1XQSUW9xVV4AvAOQCwcfRsQmm+1g/dBd0iFZO6PScKs/zAW9pnps7Lmoha/Mpj8u0negjMi5+E518FPjaVbV8UPAPP/jI8fQDlHsyOxFtLLh5ts3u3117pODPboXWif3Ao2Of/xgHz3TPOC3M4HHG/Lnhwbu63do7h4hYMO6aehZf34fmrwHeAPzE/5n8A6vPBlFYRkpAAAAAASUVORK5CYII=";
export const closeFolderIcon =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAItElEQVR4Ae2YBYzkTnPFf6/anp252d37033MjIIwsyDM4nxihUEYFoSFYeZEHGZmZmaGP/PC7YztrneolWVZDnPegqcM7ap+r6qrh//HvwX/j//H/+P/IWbwPp/ym6+P8JuAmH8APLL9z3zRC553lVe+4rkASABggwR44s1d2wZJifg14O+ZQJ//3SeXxs/83F+Uw8P8pvWat5QiAIwRYgpjJIH5Z2F9sOIlL34eJYTHgd+19E9OinqIjwS+jxGazOHSuLIZPma18lucMCQYENPjiAEbBKBFSg286AXPAZtaPfVwasxHIVqRn8k0AI8CkIb36/dJRLDZrGckM7INYJBG5+dxfHx4m4GaZuq0EMaXR2DEiIHL8ZF8xARNZl4aQ9dffdELr/HAtftQXAqQS9hIwngyO1NBj+G7M5/MQoANjJnxLDsSTNGMB37uTcfve+Ae0okqmPnkkqbXxsyMjQVNAWhsTJ6ZvguIuQByROv9D1wla84JcMGbhbfPpuP0+sIz0+elmQCqL42IoFYjeTTD42T1XEJMmPbIL099QvKIJQOTceVJhRopMTwjoVEOOCFtGJHwX4Vp+TYGtCyhTI8qxYKSFj+Pkt+TsZi5j4XxJ4wL/ukAxvay6uevG6Mp/bOqN/wLMwrQjIQMwJd+zC/pE7/yHam5IPDZmdMMGx7dC+BxqWWBlkVGZPjyj0fwy5cPNTcdB66Jl3w4mQ+OGDDz8JK9cM3/trEMhODrHoPP+3D4+58X4AaOxMe9O7zoV+V8IVn9L06wZQHM24tYuPfoD1qdfsyvwT++u/mqH1PDB7wHvO4XtP7zB6ivsGrmVLBIk2owbSNnK70wOW4XxskNmupEk/JphLCNRvJ7w8U+fu+hq7m76fNN393wzn+he/7iBbzq5NGouSHrItX/dvkI8Mw1eWY4j484xL633tQ9HX/5Fy/IZ2763vBs5TWPPqNhK2U1teZlxF5i3QY0SdBRTntkjOGcJuzknsk4Y7ZBZypxHEPe8vnXt42b+/5BiuOM0y4j08r0ZNlYUqhhsWR6pttn+ZmxrCZjYdFmV05J7j9ObvqezUvb1Hlabg8ic9qvz3VrzNssnTczWF7A5pUsmjasxueZvuV7syrmtOvUrq5oqKlMzyxIwqNWWmhSieb2VlNb8wve5Lon+4TpdimzK205yIt+p/tXLc1Qeml9LGp3l4Gc79enkpkm41wvvySz+SReGAdsFKUpOKvWxzlop6arh2q5rnRGZqqmR729AZA0+3pNs8UgMWJrzMNi0z3BwrUsYYZos1PHoZre54KqyKJaTVYDXmBhTrxe3rWQLGChTZlCSg1R6u2jknM1NfdsUuoPGtVq1czpTgjs+bHlaYyjyjqqRAuxTerN6Nl5OBVeSat91UUMNM5jcjhTrlJpjxYyMY/FZmyBBf9LStfscwJKk0pLOUhujmgYwXVcRj0R6lyujnPEMK4g9ogN40knKo3uwfPfTuDL90ia39QrTogmHApL7G2TCeJfAs/ay/iXV6umRNYaLo0cDVac0JRo6SIdnS3xS03wrt1g0tNvaZisASxu1Tz/7Q7GMyvzrH15lERTRAl+V3Kqw10jF1qalmNXnTuj+smz4avv35b3KZFvk57jQAv5oeXPy2Ms3hcSEfzt6a5+UbhkxuBQ6+Ktm1Vz4pPcuFGXT+77s7O++9DnH64+MKSX26wMLdDabkGt7cZQgGIcQAjJoPGkMb+RHJcZIyxIcIKqoEoMQA/0Er1QPzgfevJ0+MHTfniaKCmX7LXyppy4aXLt7E5Nu3HWfd2b/d8+2/0A1lpijVkbNsAaOAAOBK3v/DUBYRwggYWEzSwkwJeBWCgTUjAIekMP7O/+7QQXiJ3NDnkHHrIONcrBbZ+b1aGbviabtvWOzLasUkM/ZEQIKwEJYxLUQ3agXdqtpEYQQIDCWCCRhsUOXIAtZCAFCWTag6Qe3EPswZ3F3rAX7O3oI3OIps2ezFs+9zVp/nYffs1G3vX7lErNgrB7JMLYkMgDdie0N2pDKsYFKJZlS0YCRABe7jpBBoxsLAM1FNVQBb1xj+ix+oDOosfus1Bj6Cuuediu/Of7cPPUi7H/ruTxJjhhxcotyRm4OFVSDBVrsOjtKHI2dxiiYMtWSCLSwsEdWMxCBkCJQ7ZBOJGcUJU3oRhQVpkBZZ80g1wHqDV8OHTR12P26fOST70UN9zb8GdnW7/q9DFvVfO8Jqs2sJVKm6IqaQAK6YIUwoEJuQgsp2RAJJIWAsC2sQVVSFiETVpyglLpvPNO7jBSnUhDKLLr93VbnGH7z+4/NvfuER/6rHjnX9H2r5/Lm4fz6JFOO0LqYuWIQRF2hlTCqQijbGs4Qw0SVFkBBmxMaimJRRgJBHICxQO2Ih19yRRWOO2aUmTjzE6Z9iqPVmSL/QfNNs9f8Sj80jta8MPik94LXvgrOvz7K7zm2Qs16xIXLtqnJXfRUERplC6SB2VjRV9lt7J6vDJOCzdAWa5CVNCAQlYn5Bapd7bFMchW41A1dfBAtbXKg5A3qh52Nf/86sZnL7kOD76j+bKfRvDDwFa87u3h034D/malV/zloOezJY56ne+va+dULfeIci7XFuV1tQkuoayFHCqXcBVLUDF3EU0hSkU13Qc4rlilh7p1qc94rfD24IrztPXDnPPXr2rMyzvzRW8Lf/prABb8HHfwt4L3hE85gDf8AzxZ9MJ/6LQ+PecBbYj1odidMuQgbYMaZwwXF4IXAvcAYCeuO5b9XyMFd/AM8CDNZuOSh/g8aaIx6yNyd+YnfMHuaMuDL16Z+6v54xfDl+yBnwEwgOBbGQO2gjfCR70GnvcIvOgEuuuiBDx4wouBe7tjht2hhv6Mfw807SHN+sxPr074B4AXHkNNWF0x/3gMjzwPvv3PgT9iWqQF38kynhV8OPAkcAGt4LPfAAC/9Vf8u+CtXwkAn/vH0BvYAPcD3wVgFnAD9kuwT5uRgzQAAAAASUVORK5CYII=";
export const logoIcon =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAC+lBMVEVHcEwHBz4QJZEHBzW0mS8HBzYUD0QVGFAJCEUID2snTqQMCkDgqRS2hiLFqSrltBJIOUlkUUQMCzyOajQLCkAPDkAvIknVnxUICEMNCzoNCzpEeJTbshoHCEcNCzlqlnHgtBaXqk8JCDm4sDbJtCzosg3muhHguxnquw8HBzoHCFEHB0oHB00HCWAHCVsHBzQHB0QHBz4HB0EHCmUGC5MGB40HC2oIDpsMFaUHDooFB3UNHZEJEHcZMJcHA6AtRI8TJ5U6TIMLFIAkOpMHDYEGC3wDAZUNGIgSs/ELIbMJD64TT8AICEsH1cUe3/4G6KkbOp0NeP4JFcFFXIMUhNYRMaXU3bYTZsj+/aIAXP8oQp4WTrINIqkK9OET7PAyVZkW3vUayf8+ZJkOIZwI/M0Qm+AYm/qz8YjA53fN3sgPSdBQlvkh0Pd7secZvO8i6aPZ59YEcf/09IYpTZ9LccsPveYSK8pBUL5WqNk86ZnU9ZBI/M5r6Y69wLFneL7o+th62eIx9duZ7pl2/cg8g9i785Tj53BHe/Hs+6kXr/+Y/sKa6NKx6XdumOWj53zO53mz/L4dGmceFVMOC0QqH1YUFG8UD0tlhW9RdoIUEWMxLGKIakIUD1lDP2BsYFOvfywQDFGspTy1mDKAYT4+ephXRk0kGktfW1uaez44QG+idTDOmh0Jzs5+a011YUsNCkvXohk9MVZsVkdPSFpkbmmAgVMgGVwLyNdDUXVfUVI0J1IVvP6Rl02XbzVMVm5UWGVIOFHFkyK6iCiglUKngjeOajkqL3ReSkcTof8K3+EE58gStP8cFEoNC111W0F3lmFkUEhPPkwpsctZYm8w6Z4Nv+ORmaoQ8LQTrPoNgv8ICFYB4rVQ55QtIUyc6IHy97Z56IgHB0d2osVeY5qMd0fJ/s1OTJQTze0PueoixuQIDHAco9hW6ZNs+bQF2r8Vqf/V/bUc1f+E6pE69bNIzswNiv4Sx/MRlP8+Lk0SsvQNxd4M6aefqbtxcVxv35+rAAAAKXRSTlMA/kJCA/3+AjZCQh7+Qv78QkLOQmwPQkDw4k1C7o91QiI8O+iZiItuaoR+8TQAAAakSURBVHhe3NDVbxRhGMVhLprZJl22UChQ2EAJ+u+Oyrq7u9XdDXd3d00477dDssOUhGt+mdvzzDuz53/Ltkt9lv6+/tcDdjdoPtB/6Hf7WPtZB6i91GE0ijke6374+FBsNRaL3URf0OTk1lZ0YWGztB4Oz42lUv7p6ampdKFgd54ZhWDZHx3huFUDaLVIICBKwHp4DoLfT0C6YLcPDp4nwbw/SHMCDIEuYIL1BBLOQjC/X+RQzwmGYJwAACeQkDaEc72CzTY8wuaIBImAVpEE8wkkMKLgdOI/9H4AO0ChvQBAYkCxCKEcjW5ahLTD4ThtAoY4TuQUBYIgCBIEqSt4SWiyj3AbAhEOdKp3P3BE5EQFCYpAAtVyuUjwliE0SyUmjKVmiQiFQsFg8CSdYAD9oog9E6haTZJcLK+XhDIJubAbRCIxi2ZQxwwwIYCnC9R4F0/AvCuZJCIeb/pyjEigmUSn09k48QeQz+dBeBDteZ2fp5Ko0RgnwufLZbNu9wNUrVY3zEB3HwgEPAIAnefl+/c+ZDL1+renrO3IxAQjclmEvaqqJuAuAdjTBbrOy/KPhy/fX15eXlp6/Kndbq+0326PMwL9RBVV1Y6ZgHwvID+79ObdjQvfr7/K1C8++Xrl9eLnjyvPGyBgxH3xSqWiaQBsFsBj7B/dvnXnhRm4uhhpMGKHpWkRC4ADrnnW1nRZ/tV9ufS0cYVhuKSy3JZd1Eq0VbrIDmNMwWN7bMzQ+DLExhgDJjbYBptAGq4B0gCFkHuapHepyqJSF/wAJJZUQqJbTJRdSMAggltRkBOuEkJ1suh7zhlbHtu9SN31sQZZiPeZ7z1nbGZsk+M5goaGsaYzUMz/Mj8/39TU1NEhE5RAoFLFYo+JwPSb/2KuoKX/afUZKEATFbyTKXDNqNWq2E8QaPXmmnPDeQXP22GQaG9vlwtcapVqKqZ5XKp/aHrq/yKfoNv/2lNdnc5fzhawBtqEyfp8mAiydmGo1z/s74BB4rJcwPIaTak+YZ/0+/3397+/PdEZCoXawmH3wcFBJPJgcPy74cnmVmu1FXFra2trVgUVDJpSDPD74Df37vXeGR2tzcDX07+//vXF+9dgALB4cgUxCPQmy9hKCzhyCIIg4jAIPM87nCNfHW6sfzveHfB4rBSPJ6uCmggwQODRUANY6BJ4A+BwEMHYztrVjcP1/V+bPVVV1irgOSG7Dlxq1sAysLBCOIrUCgaO43QcFLyz9ualz9deXt241R2AgZG9iOiABp6DY8ajAZxeBzgOgmTnq0tPiGLzs2CzJDiRLaBLEDhYkHD7RAgAZxB8odXVeqr4kgg+ocgrqGkFLIF7T2Kujxd1RioIRhfrdpniNgTn/1qgt1f2zS1LuL0YAQqD4G2bna7b3V2tr392sycYhKAyR+BiE9h0XvdZiQF3s0gaiI5wMjk7PU2G6OxqdATPV1KyBOhAJtDxW1vbEuEXGAGCF+HtpeQshqhbjI6wfE22IM5G0JuMoq8tmaItgD0QA5hp+8oSGSLkpQPUUPIJtAkbJ1yJTktEkwZs4kBk+Q+miG6NNDr4ynyCj5kBHYxCbaguRcgnij7syzIU29tLbT3IGyprLJQsQbwkPUJX56rEYpTn+x4s7DFF+Gxjo5PlcwUYgRhKySo4Qs/qGa86vd7IMa4qYphz9zQ6ec5osZiBxfx2liAeRwmNlpS4M/EkxUTk6OgYCgzhxgoib7ObKXJBBQwooUYJk43jb+ysvWTs3BhaWaGKuT6c32C0mc0JcyJhfigXlDNDaitHN29tMA43e1saGqDYc/tYPpFCLiioqKAGthPitev7dz8l3N2/3juE/Jzbm8rrJXIEqJFeBrH19TlG9+Bg/4ULy121TtLfhLiW8KNWKxMUF5SXU0MJDGQGncinCTocTqdg4Iz2BE2XEnIFFcwQZ5tpMxo5oMMLX2qCAXHU19Mw/nuQl1xAkIZw0Q92wmy32xhGYLObSFyj0cToATIFhUXMUFxOlqJEBQV66lEYq20CCTI8zh3L4C0EUwblSYWCGgAMWAni0OCEtC5eCCOtSjOlKXovLYDpozJFmVQDoAgubEgYMcDC+B07VFOqU4hl3upiBKpILwZWA5QwXGoXBe/wlvycKVQqZTfbZQpFWRl6wJIxCl0TEKc2SYlPzUz8XYTkt/uIw0EolkNEFOZiKhTIfeAgYUUeCtJQAzRFOH/eRx60yIMioxb44eSpQuTzP3SdfvMfOV2IsPK/PPYp/+Yvlf+ON/5f/Akz3gsPeVUFywAAAABJRU5ErkJggg==";
================================================
FILE: src/main.css
================================================
body {
margin: 0;
min-height: 100vh;
/* overflow: hidden; */
font-size: 14px;
font-family: v-sans, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
line-height: 1.6;
-webkit-text-size-adjust: 100%;
}
.clearfix:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
.widthFull {
width: 100% !important;
}
.tac {
text-align: center;
}
.tar {
text-align: right;
}
.font12 {
font-size: 12px;
}
.font16 {
font-size: 16px;
}
.font20 {
font-size: 20px;
}
.bold {
font-weight: 900;
}
.rotate90 {
rotate: 90deg;
}
.rotate180 {
rotate: 180deg;
}
.rotate270 {
rotate: 270deg;
}
.disableUserSelect {
-moz-user-select: none;
-o-user-select:none;
-khtml-user-select:none;
-webkit-user-select:none;
-ms-user-select:none;
user-select:none;
}
/* 滚动条会一直存在,暂时先不使用 */
/* ::-webkit-scrollbar {
height: 9px;
width: 9px;
}
::-webkit-scrollbar-thumb {
border-radius: 10px;
border-style: dashed;
border-color: transparent;
border-width: 2px;
background-color: rgba(157, 165, 183, 0.4);
background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(157, 165, 183, 0.7);
} */
================================================
FILE: src/main.ts
================================================
import { createApp } from "vue";
import { create } from "naive-ui";
import { createPinia } from "pinia";
import { message } from "@tauri-apps/api/dialog";
import Debug from "debug";
import router, { goTo } from "./router";
import Root from "./root";
import { isWebMode } from "./helpers/util";
import { changeI18nLocale, LANG } from "./i18n";
import { getAppLatestRoute } from "./stores/setting";
import { getLang } from "./stores/local";
import { handleDatabaseCompatible } from "./commands/database";
import { showSplashscreen } from "./commands/window";
import { initWindowEvent } from "./event";
import "./userWorker";
// web mode enable debug:*
if (isWebMode()) {
Debug.enable("*");
}
const app = createApp(Root);
async function init() {
initWindowEvent();
showSplashscreen();
// TODO 校验数据库版本
// 判断是否需要升级级别
await handleDatabaseCompatible();
const lang = (await getLang()) || LANG.zh;
changeI18nLocale(lang);
app.use(router);
// 非浏览器模式打开上次打开的页面
if (!isWebMode()) {
const route = await getAppLatestRoute();
if (route.name) {
goTo(route.name, {
query: route.query,
});
}
}
}
const naive = create();
init()
// 初始化失败是否弹窗
.catch(console.error)
.finally(() => {
// TODO 确认客户是否允许提交此类出错信息至服务
// 便于后续优化
const unknown = "unknown";
app.config.errorHandler = (err, instance, info) => {
const name = instance?.$options.name || unknown;
const msg = (err as Error).message || unknown;
const content = `${name}(${msg}): ${info}`;
if (isWebMode()) {
console.error(content);
throw err;
} else {
message(content);
}
};
app.use(naive).use(createPinia()).mount("#app");
});
================================================
FILE: src/root.tsx
================================================
import { defineComponent, onBeforeMount, ref } from "vue";
import {
darkTheme,
NConfigProvider,
NDialogProvider,
NGlobalStyle,
NLoadingBarProvider,
NMessageProvider,
NNotificationProvider,
} from "naive-ui";
import { storeToRefs } from "pinia";
import { message } from "@tauri-apps/api/dialog";
import { closeSplashscreen } from "./commands/window";
import { useSettingStore } from "./stores/setting";
import App from "./App";
import ExLoading from "./components/ExLoading";
import { useAppStore } from "./stores/app";
import { getLocale } from "./i18n";
import { formatError } from "./helpers/util";
export default defineComponent({
name: "RootView",
setup() {
const settingStore = useSettingStore();
const appStore = useAppStore();
const { isDark } = storeToRefs(settingStore);
const processing = ref(true);
// 避免发布版本可以reload页面
if (window.location.protocol.includes("tauri")) {
document.addEventListener("contextmenu", (e) => e.preventDefault());
}
const startedAt = Date.now();
onBeforeMount(async () => {
try {
await appStore.fetch();
await settingStore.fetch();
await settingStore.resize();
} catch (err) {
message(formatError(err), "Error");
console.error(err);
} finally {
processing.value = false;
// splashscreen最多300ms
const delay = 300 - (Date.now() - startedAt);
setTimeout(closeSplashscreen, delay);
}
});
return {
processing,
isDark,
};
},
render() {
const { processing, isDark } = this;
if (processing) {
return ;
}
return (
);
},
});
================================================
FILE: src/router/index.ts
================================================
import {
createRouter,
createWebHashHistory,
LocationQueryRaw,
} from "vue-router";
import { updateAppLatestRoute } from "../stores/setting";
import { routes } from "./routes";
const history = createWebHashHistory();
const router = createRouter({
history,
routes,
});
export function goTo(
name: string,
params?: {
replace?: boolean;
query?: LocationQueryRaw;
},
): void {
router.push({
name,
replace: params?.replace,
query: params?.query,
});
}
type loadingEvent = () => void;
const noop = function () {
return;
};
let startEvent = noop;
let finishEvent = noop;
export function setLoadingEvent(
start: loadingEvent,
finish: loadingEvent,
): void {
startEvent = start;
finishEvent = finish;
}
router.beforeEach(function (to, from, next) {
if (!from || to.path !== from.path) {
startEvent();
}
next();
});
router.afterEach(function (to, from) {
if (!from || to.path !== from.path) {
finishEvent();
}
// 如果失败则忽略
updateAppLatestRoute({
name: to.name as string,
params: to.params,
query: to.query,
});
});
export default router;
================================================
FILE: src/router/routes.ts
================================================
import { Component } from "vue";
import Dashboard from "../views/Dashboard";
import Collection from "../views/Collection";
export interface Router {
path: string;
name: string;
component: Component | Promise;
}
export const names = {
home: "home",
collection: "collection",
};
export const routes: Router[] = [
{
path: "/",
name: names.home,
component: Dashboard,
},
{
path: "/collection",
name: names.collection,
component: Collection,
},
];
================================================
FILE: src/stores/api_collection.ts
================================================
import dayjs from "dayjs";
import { uniq } from "lodash-es";
import { defineStore } from "pinia";
import {
APICollection,
createAPICollection,
updateAPICollection,
listAPICollection,
deleteAPICollection,
} from "../commands/api_collection";
import {
getExpandedSettingStore,
getTopTreeItemStore,
getTabActiveStore,
} from "./local";
const tabActiveKey = "activeTabs";
interface TabActiveData {
[key: string]: string;
}
async function toggleFolderExpanded(
collection: string,
folder: string,
expanded: boolean,
) {
let items = await getExpandedSettingStore().getItem(collection);
if (!items) {
items = [];
}
if (expanded) {
items.push(folder);
} else {
items = items.filter((item) => item !== folder);
}
items = uniq(items);
await getExpandedSettingStore().setItem(collection, items);
return items;
}
export const useAPICollectionStore = defineStore("apiCollections", {
state: () => {
return {
apiCollections: [] as APICollection[],
expandedFolders: [] as string[],
topTreeItems: [] as string[],
activeTabs: {} as TabActiveData,
fetching: false,
adding: false,
updating: false,
removing: false,
};
},
actions: {
findByID(id: string) {
return this.apiCollections.find((item) => item.id === id);
},
async closeAllFolders(collection: string) {
const items: string[] = [];
await getExpandedSettingStore().setItem(collection, items);
this.expandedFolders = items;
},
async fetchExpandedFolders(collection: string) {
const items =
await getExpandedSettingStore().getItem(collection);
if (items) {
this.expandedFolders = items;
}
},
async fetchTopTreeItems(collection: string) {
const items = await getTopTreeItemStore().getItem(collection);
if (items) {
this.topTreeItems = items;
}
},
async fetchActiveTabs() {
const data =
await getTabActiveStore().getItem(tabActiveKey);
this.activeTabs = data || {};
},
getActiveTab(id: string) {
return this.activeTabs[id];
},
async updateActiveTab(params: { id: string; activeTab: string }) {
const { id, activeTab } = params;
// 已经相同
if (this.activeTabs[id] === activeTab) {
return;
}
if (activeTab) {
this.activeTabs[id] = activeTab;
} else {
delete this.activeTabs[id];
}
await getTabActiveStore().setItem(
tabActiveKey,
Object.assign({}, this.activeTabs),
);
},
async updateTopTreeItems(collection: string, idList: string[]) {
await getTopTreeItemStore().setItem(collection, idList);
this.topTreeItems = idList;
},
async openFolder(collection: string, folder: string) {
// localforage操作较快,因此不记录处理中
const items = await toggleFolderExpanded(collection, folder, true);
this.expandedFolders = items;
},
async closeFolder(collection: string, folder: string) {
// localforage操作较快,因此不记录处理中
const items = await toggleFolderExpanded(collection, folder, false);
this.expandedFolders = items;
},
async add(data: APICollection): Promise {
if (this.adding) {
return;
}
this.adding = true;
try {
await createAPICollection(data);
const arr = this.apiCollections.slice(0);
arr.push(data);
this.apiCollections = arr;
} finally {
this.adding = false;
}
},
async get(id: string): Promise {
if (!this.apiCollections.length) {
await this.fetch();
}
return this.apiCollections.find((item) => item.id === id);
},
async fetch(): Promise {
if (this.fetching) {
return;
}
this.fetching = true;
try {
this.apiCollections = await listAPICollection();
} finally {
this.fetching = false;
}
},
async update(data: APICollection): Promise {
if (this.updating) {
return;
}
this.updating = true;
data.updatedAt = dayjs().format();
try {
await updateAPICollection(data);
const arr = this.apiCollections.slice(0);
const index = arr.findIndex((item) => item.id === data.id);
if (index !== -1) {
arr[index] = data;
}
this.apiCollections = arr;
} finally {
this.updating = false;
}
},
async remove(id: string): Promise {
if (this.removing) {
return;
}
this.removing = true;
try {
await deleteAPICollection(id);
const arr = this.apiCollections.slice(0);
const index = arr.findIndex((item) => item.id === id);
if (index !== -1) {
arr.splice(index, 1);
}
this.apiCollections = arr;
} finally {
this.removing = false;
}
},
},
});
================================================
FILE: src/stores/api_folder.ts
================================================
import dayjs from "dayjs";
import { defineStore } from "pinia";
import { compact, uniq } from "lodash-es";
import {
createAPIFolder,
deleteAPIFolder,
listAPIFolder,
updateAPIFolder,
} from "../commands/api_folder";
import { APIFolder } from "../commands/api_folder";
import { useAPISettingStore } from "./api_setting";
export const useAPIFolderStore = defineStore("apiFolders", {
state: () => {
return {
apiFolders: [] as APIFolder[],
fetching: false,
adding: false,
updating: false,
removing: false,
};
},
actions: {
findByID(id: string): APIFolder {
const index = this.apiFolders.findIndex((item) => item.id === id);
return this.apiFolders[index];
},
async updateByID(id: string, data: unknown) {
const index = this.apiFolders.findIndex((item) => item.id === id);
const item = Object.assign(this.apiFolders[index], data);
await this.update(item);
},
async add(data: APIFolder): Promise {
if (this.adding) {
return;
}
this.adding = true;
try {
await createAPIFolder(data);
const arr = this.apiFolders.slice(0);
arr.push(data);
this.apiFolders = arr;
} finally {
this.adding = false;
}
},
async fetch(collection: string): Promise {
if (this.fetching) {
return;
}
this.fetching = true;
try {
const arr = await listAPIFolder(collection);
arr.forEach((item) => {
if (!item.children) {
return;
}
item.children = uniq(item.children.split(",")).join(",");
});
this.apiFolders = arr;
} finally {
this.fetching = false;
}
},
async addChild(params: {
// folder的id(为空则表示添加至顶层)
id: string;
// 要添加的元素
children: string[];
// 添加在哪个元素之前
before?: string;
}) {
if (this.updating) {
return;
}
this.updating = true;
const { id, children } = params;
const { apiFolders } = this;
// 如果folder不存在
const currentFolder = apiFolders.find((item) => item.id === id);
const omitChild = (children: string, child: string) => {
const arr = children.split(",").filter((item) => item !== child);
return uniq(compact(arr)).join(",");
};
const addChild = (
children: string,
child: string,
beforeItem: string,
) => {
const arr = compact(children.split(","));
const index = arr.indexOf(beforeItem);
if (index === -1) {
arr.push(child);
} else {
arr.splice(index, 0, child);
}
return uniq(compact(arr)).join(",");
};
const updateData: Map = new Map();
for (let i = 0; i < children.length; i++) {
const child = children[i];
// 查找此child是否有已有对应folder
apiFolders.forEach((folder) => {
if (folder.children?.includes(child)) {
folder.children = omitChild(folder.children, child);
updateData.set(folder.id, folder);
}
});
if (currentFolder) {
currentFolder.children = addChild(
currentFolder.children,
child,
params.before || "",
);
updateData.set(currentFolder.id, currentFolder);
}
}
const list: APIFolder[] = [];
updateData.forEach((value) => {
list.push(value);
});
try {
for (let i = 0; i < list.length; i++) {
await updateAPIFolder(list[i]);
}
} finally {
this.updating = false;
}
},
async update(data: APIFolder): Promise {
if (this.updating) {
return;
}
this.updating = true;
data.updatedAt = dayjs().format();
try {
await updateAPIFolder(data);
const arr = this.apiFolders.slice(0);
const index = arr.findIndex((item) => item.id === data.id);
if (index !== -1) {
arr[index] = data;
}
this.apiFolders = arr;
} finally {
this.updating = false;
}
},
async remove(id: string): Promise {
if (this.removing) {
return;
}
this.removing = true;
try {
const settingStore = useAPISettingStore();
const result = await deleteAPIFolder(id);
const folderIds = result.folders || [];
const settingIds = result.settings || [];
this.apiFolders = this.apiFolders.filter(
(item) => !folderIds.includes(item.id),
);
settingStore.apiSettings = settingStore.apiSettings.filter((item) => {
return !settingIds.includes(item.id);
});
} finally {
this.removing = false;
}
},
},
});
================================================
FILE: src/stores/api_setting.ts
================================================
import { defineStore } from "pinia";
import dayjs from "dayjs";
import {
APISetting,
listAPISetting,
createAPISetting,
updateAPISetting,
deleteAPISettings,
} from "../commands/api_setting";
import { HTTPRequest } from "../commands/http_request";
import { getAPISettingStore } from "./local";
import { useEnvironmentStore, ENVRegexp } from "./environment";
import { isWebMode, setAppTitle } from "../helpers/util";
import { useGlobalReqHeaderStore } from "./global_req_header";
import { cloneDeep } from "lodash-es";
const selectedIDKey = "selectedID";
export enum SettingType {
HTTP = "http",
Folder = "folder",
}
export const useAPISettingStore = defineStore("apiSettings", {
state: () => {
return {
selectedID: "",
apiSettings: [] as APISetting[],
fetching: false,
adding: false,
updating: false,
removing: false,
};
},
actions: {
async setWindowTitle(id: string) {
if (isWebMode()) {
return;
}
const result = this.findByID(id);
if (!result) {
return;
}
await setAppTitle(result.name);
},
select(id: string) {
// 设置失败则忽略,仅输出日志
getAPISettingStore().setItem(selectedIDKey, id).catch(console.error);
this.selectedID = id;
this.setWindowTitle(this.selectedID);
},
getHTTPRequest(id: string) {
const setting = this.findByID(id);
if (!setting) {
return {} as HTTPRequest;
}
return JSON.parse(setting.setting || "{}") as HTTPRequest;
},
fillValues(req: HTTPRequest) {
const arr = ENVRegexp.exec(req.uri);
if (arr?.length === 2) {
const envValue = useEnvironmentStore().getValue(arr[1]);
if (envValue) {
req.uri = req.uri.replace(arr[0], envValue);
}
}
const variables = useGlobalReqHeaderStore().listEnable();
if (variables) {
if (!req.headers) {
req.headers = [];
}
variables.forEach((item) => {
req.headers.push({
key: item.name,
value: item.value,
enabled: true,
});
});
}
},
getHTTPRequestFillValues(id: string) {
const req = this.getHTTPRequest(id);
const originalReq = cloneDeep(req);
if (!req.uri) {
return {
originalReq,
req,
};
}
this.fillValues(req);
return {
originalReq,
req,
};
},
findByID(id: string): APISetting {
const index = this.apiSettings.findIndex((item) => item.id === id);
return this.apiSettings[index];
},
async updateByID(id: string, data: unknown) {
const index = this.apiSettings.findIndex((item) => item.id === id);
const item = Object.assign(this.apiSettings[index], data);
await this.update(item);
},
async add(data: APISetting) {
if (this.adding) {
return;
}
this.adding = true;
try {
await createAPISetting(data);
const arr = this.apiSettings.slice(0);
arr.push(data);
this.apiSettings = arr;
} finally {
this.adding = false;
}
},
async fetch(collection: string): Promise {
if (this.fetching) {
return;
}
this.fetching = true;
try {
// 先获取所有api setting,再获取选中id
this.apiSettings = await listAPISetting(collection);
this.selectedID = (await getAPISettingStore().getItem(
selectedIDKey,
)) as string;
this.setWindowTitle(this.selectedID);
} finally {
this.fetching = false;
}
},
async update(data: APISetting) {
if (this.updating) {
return;
}
this.updating = true;
try {
data.updatedAt = dayjs().format();
await updateAPISetting(data);
const arr = this.apiSettings.slice(0);
let found = -1;
arr.forEach((item, index) => {
if (item.id === data.id) {
found = index;
}
});
if (found !== -1) {
arr[found] = data;
}
this.apiSettings = arr;
} finally {
this.updating = false;
}
},
async remove(id: string) {
if (this.removing) {
return;
}
this.removing = true;
try {
await deleteAPISettings([id]);
this.apiSettings = this.apiSettings.filter((item) => item.id !== id);
// 如果删除了该id,则清空
if (id === this.selectedID) {
this.select("");
}
} finally {
this.removing = false;
}
},
},
});
================================================
FILE: src/stores/app.ts
================================================
import { defineStore } from "pinia";
import { getVersion, getTauriVersion } from "@tauri-apps/api/app";
import { arch, platform, type, version } from "@tauri-apps/api/os";
import { appDataDir } from "@tauri-apps/api/path";
import { isWebMode } from "../helpers/util";
import { getUserAgent } from "../commands/http_request";
export const useAppStore = defineStore("app", {
state: () => {
return {
version: "--",
tauriVersion: "--",
arch: "--",
platform: "--",
os: "--",
osVersion: "--",
dir: "--",
userAgent: "--",
};
},
actions: {
async fetch() {
if (!isWebMode()) {
this.version = await getVersion();
this.tauriVersion = await getTauriVersion();
this.arch = await arch();
this.platform = await platform();
this.os = await type();
this.osVersion = await version();
this.dir = await appDataDir();
this.userAgent = await getUserAgent();
}
},
},
});
================================================
FILE: src/stores/cookie.ts
================================================
import { sortBy } from "lodash-es";
import { defineStore } from "pinia";
import {
listCookie,
Cookie,
deleteCookie,
clearCookie,
addOrUpdate,
} from "../commands/cookies";
function isSameCookie(c1: Cookie, c2: Cookie) {
return c1.name === c2.name && c1.domain === c2.domain && c1.path === c2.path;
}
export const useCookieStore = defineStore("cookie", {
state: () => {
return {
cookies: [] as Cookie[],
fetching: false,
removing: false,
adding: false,
};
},
actions: {
async fetch() {
if (this.fetching) {
return;
}
this.fetching = true;
try {
const cookies = await listCookie();
this.cookies = sortBy(cookies, (c) => c.name);
} finally {
this.fetching = false;
}
},
async remove(c: Cookie) {
if (this.removing) {
return;
}
this.removing = true;
try {
await deleteCookie(c);
const cookies = this.cookies.slice(0).filter((item) => {
return !isSameCookie(item, c);
});
this.cookies = cookies;
} finally {
this.removing = false;
}
},
async clear() {
if (this.removing) {
return;
}
this.removing = true;
try {
await clearCookie();
this.cookies = [];
} finally {
this.removing = false;
}
},
async addOrUpdate(c: Cookie) {
if (this.adding) {
return;
}
this.adding = true;
try {
await addOrUpdate(c);
const arr = this.cookies.slice(0);
let found = -1;
arr.forEach((item, index) => {
if (isSameCookie(item, c)) {
found = index;
}
});
if (found === -1) {
arr.push(c);
} else {
arr[found] = c;
}
this.cookies = arr;
} finally {
this.adding = false;
}
},
},
});
================================================
FILE: src/stores/dialog.ts
================================================
import { defineStore } from "pinia";
export const useDialogStore = defineStore("dialogs", {
state: () => {
return {
showSetting: false,
showCookie: false,
showEnvironment: false,
showStore: false,
showCustomizeVariableStore: false,
showReqHeader: false,
};
},
actions: {
toggleSettingDialog(shown: boolean) {
this.showSetting = shown;
},
toggleCookieDialog(shown: boolean) {
this.showCookie = shown;
},
toggleEnvironmentDialog(shown: boolean) {
this.showEnvironment = shown;
},
toggleStoreDialog(shown: boolean) {
this.showStore = shown;
},
toggleCustomizeVariableDialog(shown: boolean) {
this.showCustomizeVariableStore = shown;
},
toggleReqHeaderDialog(shown: boolean) {
this.showReqHeader = shown;
},
},
});
================================================
FILE: src/stores/environment.ts
================================================
import { VariableCategory } from "../commands/variable";
import { newVariableStore } from "./variable";
export const ENVRegexp = /\{\{([\S\s]+)\}\}/;
export const useEnvironmentStore = newVariableStore(
"environments",
VariableCategory.Environment,
);
================================================
FILE: src/stores/global_req_header.ts
================================================
import { VariableCategory } from "../commands/variable";
import { newVariableStore } from "./variable";
export const useGlobalReqHeaderStore = newVariableStore(
"globalReqHeaders",
VariableCategory.GlobalReqHeaders,
);
================================================
FILE: src/stores/header.ts
================================================
import { defineStore } from "pinia";
interface Breadcrumb {
route: string;
name: string;
}
export const useHeaderStore = defineStore("header", {
state: () => {
return {
breadcrumbs: [] as Breadcrumb[],
};
},
actions: {
add(breadcrumb: Breadcrumb) {
const { breadcrumbs } = this;
if (
breadcrumbs.length &&
breadcrumbs[breadcrumbs.length - 1].route === breadcrumb.route
) {
return;
}
const arr = breadcrumbs.slice(0);
arr.push(breadcrumb);
this.breadcrumbs = arr;
},
clear() {
this.breadcrumbs = [];
},
},
});
================================================
FILE: src/stores/local.ts
================================================
import localforage from "localforage";
const stores: Map = new Map();
function createNewStore(name: string) {
let store: LocalForage;
return function () {
if (!store) {
store = localforage.createInstance({
name,
});
}
stores.set(name, store);
return store;
};
}
export enum StoreKey {
expandedSetting = "expandedSetting",
topTreeItem = "topTreeItem",
tabActive = "tabActive",
apiSetting = "apiSetting",
setting = "setting",
pinRequests = "pinRequests",
latestResponse = "latestResponse",
lang = "lang",
}
// 记录展开配置项
export const getExpandedSettingStore = createNewStore(StoreKey.expandedSetting);
// 记录顶层的配置项
export const getTopTreeItemStore = createNewStore(StoreKey.topTreeItem);
// 记录活动的Tab
export const getTabActiveStore = createNewStore(StoreKey.tabActive);
// API Setting的额外记录,如选中记录等
export const getAPISettingStore = createNewStore(StoreKey.apiSetting);
// 应用配置项
export const getSettingStore = createNewStore(StoreKey.setting);
// Pin的API配置
export const getPinRequestStore = createNewStore(StoreKey.pinRequests);
// 最新请求响应
export const getLatestResponseStore = createNewStore(StoreKey.latestResponse);
const langKey = "lang";
const getLangStore = createNewStore(StoreKey.lang);
// 语言配置
export async function getLang() {
const lang = await getLangStore().getItem(langKey);
if (lang) {
return lang as string;
}
const arr = window.navigator.language?.split("-");
return arr[0] || "";
}
export async function setLang(lang: string) {
await getLangStore().setItem(langKey, lang);
}
export async function clearStore(name: StoreKey) {
const s = stores.get(name);
if (!s) {
return;
}
await s.clear();
}
================================================
FILE: src/stores/pin_request.ts
================================================
import { defineStore } from "pinia";
import { getPinRequestStore } from "./local";
interface LatestRequest {
id: string;
}
export const usePinRequestStore = defineStore("pinRequest", {
state: () => {
return {
fetching: false,
currentCollection: "",
requests: [] as LatestRequest[],
};
},
actions: {
async fetch(collection: string) {
if (this.fetching) {
return;
}
this.fetching = true;
try {
const result = await getPinRequestStore().getItem(collection);
this.requests = (result || []) as LatestRequest[];
this.currentCollection = collection;
} finally {
this.fetching = false;
}
},
async save() {
const { currentCollection, requests } = this;
const arr = requests.map((item) => Object.assign({}, item));
await getPinRequestStore().setItem(currentCollection, arr);
},
async add(collection: string, req: LatestRequest) {
const { currentCollection, requests } = this;
if (currentCollection !== collection) {
return;
}
const found = requests.find((item) => item.id === req.id);
if (found) {
return;
}
requests.push(req);
await this.save();
},
async remove(id: string) {
const { requests } = this;
const arr = requests.filter((item) => item.id !== id);
this.requests = arr;
await this.save();
},
},
});
================================================
FILE: src/stores/setting.ts
================================================
import { defineStore } from "pinia";
import { appWindow } from "@tauri-apps/api/window";
import { setWindowSize } from "../commands/window";
import { isWebMode } from "../helpers/util";
import { LocationQuery, RouteParams } from "vue-router";
import { getSettingStore } from "./local";
export interface Timeout {
connect: number;
write: number;
read: number;
}
interface AppSetting {
theme: string;
collectionSortType: string;
collectionColumnWidths: number[];
resizeType: string;
size: {
width: number;
height: number;
};
latestRoute: {
name: string;
params: RouteParams;
query: LocationQuery;
};
timeout: Timeout;
}
export enum ResizeType {
Max = "max",
Custom = "custom",
}
const appSettingKey = "app";
async function getAppSetting(): Promise {
const setting = await getSettingStore().getItem(appSettingKey);
if (setting) {
return setting;
}
return {} as AppSetting;
}
function updateAppSetting(data: AppSetting): Promise {
return getSettingStore().setItem(appSettingKey, data);
}
export async function updateAppLatestRoute(route: {
name: string;
params: RouteParams;
query: LocationQuery;
}) {
const setting = await getAppSetting();
setting.latestRoute = route;
await updateAppSetting(setting);
}
export async function getAppLatestRoute() {
const setting = await getAppSetting();
return setting.latestRoute;
}
function isDarkTheme(theme: string) {
return theme === "dark";
}
export const useSettingStore = defineStore("common", {
state: () => {
return {
fetching: false,
theme: "",
isDark: false,
systemTheme: "",
collectionSortType: "",
// collection页面的分栏宽度
collectionColumnWidths: [] as number[],
resizeType: "",
// 展示尺寸
size: {
width: 0,
height: 0,
},
timeout: {
connect: 0,
read: 0,
write: 0,
},
};
},
actions: {
async fetch(): Promise {
if (this.fetching) {
return;
}
this.fetching = true;
try {
// 优先使用用户设置
const setting = await getAppSetting();
let currentTheme = setting.theme || "";
this.theme = currentTheme;
if (setting.collectionColumnWidths?.length) {
this.collectionColumnWidths = setting.collectionColumnWidths;
}
if (!isWebMode()) {
const result = await appWindow.theme();
this.systemTheme = (result as string) || "";
if (!currentTheme) {
currentTheme = this.systemTheme;
}
}
this.isDark = isDarkTheme(currentTheme);
this.collectionSortType = setting.collectionSortType;
// 如果为空
// 设置默认值
if (!this.collectionColumnWidths.length) {
// 左侧,中间,右侧自动填充
const first = 300;
this.collectionColumnWidths = [first, 0.5];
}
if (setting.size) {
this.size = setting.size;
}
this.resizeType = setting.resizeType || ResizeType.Max;
this.timeout = Object.assign(
{
connect: 0,
write: 0,
read: 0,
},
setting.timeout,
);
} catch (err) {
// 获取失败则忽略
} finally {
this.fetching = false;
}
},
async updateTheme(theme: string) {
const setting = await getAppSetting();
setting.theme = theme;
await updateAppSetting(setting);
this.theme = theme;
// 如果theme 为空表示使用系统主题
this.isDark = isDarkTheme(theme || this.systemTheme);
},
async updateCollectionSortType(sortType: string) {
const setting = await getAppSetting();
setting.collectionSortType = sortType;
await updateAppSetting(setting);
this.collectionSortType = sortType;
},
async updateCollectionColumnWidths(widths: number[]) {
const setting = await getAppSetting();
setting.collectionColumnWidths = widths;
await updateAppSetting(setting);
this.collectionColumnWidths = widths;
},
async updateParamsColumnWidth(width: number) {
if (width < 0.2 || width > 0.8) {
return;
}
const setting = await getAppSetting();
const widths = setting.collectionColumnWidths.slice(0);
widths[1] = width;
return this.updateCollectionColumnWidths(widths);
},
async updateSize(width: number, height: number) {
const setting = await getAppSetting();
setting.size = {
width,
height,
};
await updateAppSetting(setting);
this.size = {
width,
height,
};
},
async updateResizeType(resizeType: string) {
const setting = await getAppSetting();
setting.resizeType = resizeType;
await updateAppSetting(setting);
this.resizeType = resizeType;
},
async resize() {
const { width, height } = this.size;
if (this.resizeType === ResizeType.Max) {
await setWindowSize(-1, -1);
} else if (width > 0 && height > 0) {
await setWindowSize(width, height);
}
},
getRequestTimeout() {
return this.timeout;
},
async updateRequestTimeout(params: Timeout) {
const setting = await getAppSetting();
setting.timeout = params;
await updateAppSetting(setting);
this.timeout = setting.timeout;
},
},
});
================================================
FILE: src/stores/variable.ts
================================================
import dayjs from "dayjs";
import { sortBy } from "lodash-es";
import { defineStore } from "pinia";
import {
createVariable,
deleteVariable,
listVariable,
updateVariable,
Variable,
VariableCategory,
VariableStatus,
} from "../commands/variable";
export const useCustomizeStore = newVariableStore(
"customizeVariables",
VariableCategory.Customize,
);
export function newVariableStore(name: string, category: string) {
return defineStore(name, {
state: () => {
return {
variables: [] as Variable[],
fetching: false,
adding: false,
updating: false,
removing: false,
};
},
actions: {
getValue(name: string) {
const value = this.variables.find((item) => {
return item.enabled === VariableStatus.Enabled && item.name === name;
});
return value?.value;
},
async add(value: Variable) {
if (this.adding) {
return;
}
this.adding = true;
try {
value.category = category;
await createVariable(value);
this.variables.push(value);
} finally {
this.adding = false;
}
},
async fetch(collection: string) {
if (this.fetching) {
return;
}
this.fetching = true;
try {
const result = await listVariable(collection, category);
this.variables = sortBy(result, (item) => item.name);
} finally {
this.fetching = false;
}
},
listEnable(): Variable[] {
return this.variables.filter(
(item) => item.enabled === VariableStatus.Enabled,
);
},
async update(value: Variable) {
if (this.updating) {
return;
}
this.updating = true;
try {
value.updatedAt = dayjs().format();
value.category = category;
await updateVariable(value);
const arr = this.variables.slice(0);
let found = -1;
arr.forEach((item, index) => {
if (item.id === value.id) {
found = index;
}
});
if (found !== -1) {
arr[found] = value;
}
this.variables = arr;
} finally {
this.updating = false;
}
},
async remove(id: string) {
if (this.removing) {
return;
}
this.removing = true;
try {
await deleteVariable([id]);
this.variables = this.variables.filter((item) => item.id !== id);
} finally {
this.removing = false;
}
},
},
});
}
================================================
FILE: src/userWorker.ts
================================================
import * as monaco from "monaco-editor";
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
self.MonacoEnvironment = {
// eslint-disable-next-line
getWorker(_: any, label: string) {
if (label === "json") {
return new jsonWorker();
}
if (label === "css" || label === "scss" || label === "less") {
return new cssWorker();
}
if (label === "html" || label === "handlebars" || label === "razor") {
return new htmlWorker();
}
if (label === "typescript" || label === "javascript") {
return new tsWorker();
}
return new editorWorker();
},
};
monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true);
================================================
FILE: src/views/AppHeader.tsx
================================================
import {
NBreadcrumb,
NBreadcrumbItem,
NDivider,
NIcon,
NTab,
NTabs,
NDropdown,
NSpace,
useMessage,
useDialog,
} from "naive-ui";
import { css } from "@linaria/core";
import { defineComponent, watch, ref, StyleValue, onBeforeUnmount } from "vue";
import { storeToRefs } from "pinia";
import {
AppsOutline,
BowlingBallOutline,
CodeSlashOutline,
CodeWorkingOutline,
DownloadOutline,
LanguageOutline,
ServerOutline,
SettingsOutline,
} from "@vicons/ionicons5";
import { useRoute } from "vue-router";
import { open } from "@tauri-apps/api/dialog";
import { mainHeaderHeight } from "../constants/style";
import { getCurrentLang, i18nCommon, i18nSetting, LANG } from "../i18n";
import { names } from "../router/routes";
import { goTo } from "../router";
import { useHeaderStore } from "../stores/header";
import { useDialogStore } from "../stores/dialog";
import { useSettingStore } from "../stores/setting";
import { usePinRequestStore } from "../stores/pin_request";
import { useAPISettingStore } from "../stores/api_setting";
import { setLang } from "../stores/local";
import { reload, showError } from "../helpers/util";
import { logoIcon } from "../icons";
import { exportTables, importTables } from "../commands/database";
const logoWidth = 300;
const headerClass = css`
position: relative;
height: ${mainHeaderHeight}px;
line-height: ${mainHeaderHeight}px;
.loggWrapper {
float: left;
}
.logo {
margin-left: 10px;
float: left;
font-weight: 600;
background-image: url(${logoIcon});
background-repeat: no-repeat;
background-size: 30px;
background-position: left center;
padding-left: 40px;
div {
margin-left: 10px !important;
}
}
.breadcrumb {
padding-top: ${(mainHeaderHeight - 25) / 2}px;
}
.pinApis {
left: ${logoWidth}px;
position: absolute;
right: 120px;
padding-top: 12px;
.n-tabs-pad {
border-bottom: none !important;
}
}
.funcs {
float: right;
padding-top: 6px;
margin-right: 30px;
text-align: right;
.n-icon {
padding: 5px;
font-size: 20px;
font-weight: 900;
}
}
`;
enum FnKey {
cookie = "cookie",
store = "store",
env = "env",
customizeVariable = "customizeVariable",
setting = "setting",
exportTables = "expxortTables",
importTables = "importTables",
reqHeader = "reqHeader",
none = "none",
}
export default defineComponent({
name: "AppHeaderView",
setup() {
const message = useMessage();
const dialog = useDialog();
const headerStore = useHeaderStore();
const dialogStore = useDialogStore();
const route = useRoute();
const settingStore = useSettingStore();
const apiSettingStore = useAPISettingStore();
const pinRequestStore = usePinRequestStore();
const { requests } = storeToRefs(pinRequestStore);
const activePinRequest = ref("");
let activePinRequestID = "";
const { collectionColumnWidths } = storeToRefs(settingStore);
const currentRoute = ref(route.name);
watch(
() => route.name,
(value) => {
currentRoute.value = value;
},
);
const { breadcrumbs } = storeToRefs(headerStore);
const stop = watch(
() => apiSettingStore.selectedID,
(id) => {
// 如果不是当前的tab,是置空
if (id !== activePinRequestID) {
activePinRequest.value = "";
pinRequestStore.requests.forEach((item, index) => {
if (item.id === id) {
const result = apiSettingStore.findByID(id);
if (!result) {
return;
}
activePinRequest.value = `${index + 1}: ${result.name}`;
}
});
}
},
);
onBeforeUnmount(() => {
stop();
});
const getRequest = (name: string) => {
const arr = name.split(":");
const index = Number(arr[0]) - 1;
return pinRequestStore.requests[index];
};
const handleSelectePinRequest = (name: string) => {
const req = getRequest(name);
if (!req) {
return;
}
apiSettingStore.select(req.id);
activePinRequest.value = name;
activePinRequestID = req.id;
};
const handleRemovePinRequest = (name: string) => {
const req = getRequest(name);
if (!req) {
return;
}
pinRequestStore.remove(req.id);
};
const handleBackup = async () => {
const d = message.loading(i18nSetting("exportTablesProcessing"), {
duration: 60 * 1000,
});
try {
const filename = await exportTables();
const msg = i18nSetting("exportTablesSuccess").replace("%s", filename);
message.info(msg);
} finally {
d.destroy();
}
};
const confirmRestore = async (file: string) => {
try {
await importTables(file);
message.info(i18nSetting("importTablesSuccess"));
setTimeout(reload, 3000);
} catch (err) {
showError(message, err);
}
};
const handleRestore = async () => {
try {
const selected = await open({
title: i18nCommon("selectFile"),
filters: [
{
name: "zip",
extensions: ["zip"],
},
],
});
if (selected) {
dialog.warning({
title: i18nSetting("importTables"),
content: i18nSetting("importTablesTips"),
onPositiveClick() {
confirmRestore(selected as string);
},
positiveText: i18nCommon("confirm"),
});
}
} catch (err) {
showError(message, err);
}
};
const handleFunction = (key: string) => {
switch (key) {
case FnKey.cookie:
dialogStore.toggleCookieDialog(true);
break;
case FnKey.store:
dialogStore.toggleStoreDialog(true);
break;
case FnKey.env:
dialogStore.toggleEnvironmentDialog(true);
break;
case FnKey.setting:
dialogStore.toggleSettingDialog(true);
break;
case FnKey.customizeVariable:
dialogStore.toggleCustomizeVariableDialog(true);
break;
case FnKey.reqHeader:
dialogStore.toggleReqHeaderDialog(true);
break;
case FnKey.exportTables:
handleBackup();
break;
case FnKey.importTables:
handleRestore();
break;
default:
break;
}
};
const handleChangeLang = async (lang: string) => {
if (lang === getCurrentLang()) {
return;
}
try {
await setLang(lang);
message.info(i18nSetting("langChangeSuccess"));
setTimeout(() => {
reload();
}, 3000);
} catch (err) {
showError(message, err);
}
};
return {
requests,
activePinRequest,
collectionColumnWidths,
currentRoute,
breadcrumbs,
handleFunction,
handleSelectePinRequest,
handleRemovePinRequest,
handleChangeLang,
findByID: apiSettingStore.findByID,
};
},
render() {
const {
requests,
collectionColumnWidths,
breadcrumbs,
$route,
findByID,
currentRoute,
activePinRequest,
} = this;
const arr = [
{
route: names.home,
name: i18nCommon("dashboard"),
},
];
arr.push(...breadcrumbs);
const items = arr.map((item) => {
return (
{
if (item.route === $route.name) {
return;
}
goTo(item.route);
}}
>
{item.name}
);
});
const pinApisStyle: StyleValue = {};
if (
collectionColumnWidths.length &&
collectionColumnWidths[0] > logoWidth
) {
pinApisStyle["left"] = `${collectionColumnWidths[0] + 2}px`;
}
const getTabs = () => {
return requests.map((item, index) => {
const result = findByID(item.id);
if (!result) {
return;
}
const name = `${index + 1}: ${result.name}`;
return ;
});
};
const options = [
{
label: i18nSetting("cookieSetting"),
key: FnKey.cookie,
type: "",
icon: () => (
),
},
{
label: i18nSetting("storeSetting"),
key: FnKey.store,
icon: () => (
),
},
{
label: i18nSetting("appSetting"),
key: FnKey.setting,
icon: () => (
),
},
];
switch (currentRoute) {
case names.home:
{
options.push(
{
label: "",
type: "divider",
key: FnKey.none,
icon: () => ,
},
{
label: i18nSetting("exportTables"),
key: FnKey.exportTables,
icon: () => (
),
},
{
label: i18nSetting("importTables"),
key: FnKey.importTables,
icon: () => (
),
},
);
}
break;
case names.collection:
{
options.unshift(
{
label: i18nSetting("envSetting"),
key: FnKey.env,
icon: () => (
),
},
{
label: i18nSetting("customizeVariableSetting"),
key: FnKey.customizeVariable,
icon: () => (
),
},
{
label: i18nSetting("reqHeaderSetting"),
key: FnKey.reqHeader,
icon: () => H,
},
{
label: "",
type: "divider",
key: FnKey.none,
icon: () => ,
},
);
}
break;
default:
break;
}
const langs = [
{
label: "中文",
key: LANG.zh,
},
{
label: "English",
key: LANG.en,
},
{
label: "Українська",
key: LANG.uk,
},
];
return (
);
},
});
================================================
FILE: src/views/AppSetting.tsx
================================================
import { defineComponent, onBeforeUnmount } from "vue";
import {
NCard,
NDescriptions,
NDescriptionsItem,
NDivider,
NFormItem,
NGi,
NGrid,
NInputNumber,
NP,
NRadio,
NRadioGroup,
NSpace,
useMessage,
} from "naive-ui";
import { getNormalDialogStyle, showError } from "../helpers/util";
import { i18nSetting } from "../i18n";
import { useSettingStore, ResizeType } from "../stores/setting";
import { storeToRefs } from "pinia";
import { useAppStore } from "../stores/app";
export default defineComponent({
name: "AppSettingView",
setup() {
const settingStore = useSettingStore();
const appStore = useAppStore();
const message = useMessage();
const { theme, size, resizeType, timeout } = storeToRefs(settingStore);
const updateTheme = async (value: string) => {
try {
await settingStore.updateTheme(value);
} catch (err) {
showError(message, err);
}
};
let resized = false;
onBeforeUnmount(() => {
if (!resized) {
return;
}
settingStore.resize();
});
const updateSize = async (value: number, category: string) => {
try {
let { width, height } = settingStore.size;
if (category == "width") {
width = value;
} else {
height = value;
}
await settingStore.updateSize(width, height);
resized = true;
} catch (err) {
showError(message, err);
}
};
const updateResizeType = async (value: string) => {
try {
resized = true;
await settingStore.updateResizeType(value);
} catch (err) {
showError(message, err);
}
};
const updateTimeout = async (category: string, value: number) => {
try {
const timeout = Object.assign({}, settingStore.timeout);
switch (category) {
case "connect":
timeout.connect = value;
break;
case "write":
timeout.write = value;
break;
case "read":
timeout.read = value;
break;
default:
break;
}
await settingStore.updateRequestTimeout(timeout);
} catch (err) {
showError(message, err);
}
};
return {
timeout,
theme,
size,
resizeType,
infos: [
{
name: i18nSetting("appVersion"),
value: appStore.version,
},
{
name: "Tauri",
value: appStore.tauriVersion,
},
{
name: i18nSetting("platform"),
value: appStore.platform,
},
{
name: i18nSetting("os"),
value: appStore.os,
},
{
name: i18nSetting("osVersion"),
value: appStore.osVersion,
},
{
name: i18nSetting("arch"),
value: appStore.arch,
},
{
name: i18nSetting("userAgent"),
value: appStore.userAgent,
span: 3,
},
{
name: i18nSetting("dir"),
value: appStore.dir,
span: 3,
},
{
name: i18nSetting("browser"),
value: window.navigator.userAgent,
span: 3,
},
],
updateSize,
updateTheme,
updateResizeType,
updateTimeout,
};
},
render() {
const modalStyle = getNormalDialogStyle();
const { theme, size, resizeType, updateSize, updateResizeType, timeout } =
this;
const descriptionItems = this.infos.map((item) => {
return (
{item.value}
);
});
return (
{i18nSetting("themeTitle")}
{i18nSetting("windowSize")}
{
updateSize(value || 0, "width");
}}
/>
{
updateSize(value || 0, "height");
}}
/>
{i18nSetting("timeoutSetting")}
{
this.updateTimeout("connect", value || 0);
}}
/>
{
this.updateTimeout("read", value || 0);
}}
/>
{
this.updateTimeout("write", value || 0);
}}
/>
{i18nSetting("infoTitle")}
{descriptionItems}
);
},
});
================================================
FILE: src/views/Collection.tsx
================================================
import { useMessage } from "naive-ui";
import {
defineComponent,
onBeforeMount,
onBeforeUnmount,
ref,
watch,
} from "vue";
import { useRoute } from "vue-router";
import { storeToRefs } from "pinia";
import { css } from "@linaria/core";
import { ulid } from "ulid";
import { debounce } from "lodash-es";
import { getBodyWidth, showError } from "../helpers/util";
import { useAPICollectionStore } from "../stores/api_collection";
import ExLoading from "../components/ExLoading";
import { useHeaderStore } from "../stores/header";
import { useSettingStore } from "../stores/setting";
import { mainHeaderHeight } from "../constants/style";
import ExColumn from "../components/ExColumn";
import APISettingTree from "../components/APISettingTree";
import APISettingParams from "../components/APISettingParams";
import { useEnvironmentStore } from "../stores/environment";
import { useGlobalReqHeaderStore } from "../stores/global_req_header";
import { useAPISettingStore } from "../stores/api_setting";
import { abortRequestID, doHTTPRequest } from "../commands/http_request";
import {
HTTPResponse,
getLatestResponse,
onSelectResponse,
} from "../commands/http_response";
import APIResponse from "../components/APIResponse";
import { usePinRequestStore } from "../stores/pin_request";
import { useAPIFolderStore } from "../stores/api_folder";
const contentClass = css`
position: fixed;
left: 0;
right: 0;
top: ${mainHeaderHeight + 2}px;
bottom: 0;
`;
export default defineComponent({
name: "CollectionView",
setup() {
const route = useRoute();
const collection = route.query.collection as string;
const message = useMessage();
const headerStore = useHeaderStore();
const settingStore = useSettingStore();
const apiFolderStore = useAPIFolderStore();
const apiSettingStore = useAPISettingStore();
const { collectionColumnWidths } = storeToRefs(settingStore);
const { selectedID } = storeToRefs(apiSettingStore);
const processing = ref(false);
const sending = ref(false);
const response = ref({} as HTTPResponse);
const maxWidth = ref(window.innerWidth);
const stop = watch(selectedID, async (id) => {
const resp = await getLatestResponse(id);
if (resp) {
response.value = resp;
} else {
// 如果选择新的api,则重置数据
response.value = {
api: id,
} as HTTPResponse;
}
});
const handleResize = debounce(
() => {
// 如果调整过少,则不触发
if (Math.abs(maxWidth.value - window.innerWidth) >= 30) {
maxWidth.value = window.innerWidth;
}
},
100,
{
leading: false,
trailing: true,
},
);
onBeforeMount(async () => {
processing.value = true;
try {
await usePinRequestStore().fetch(collection);
await apiFolderStore.fetch(collection);
await apiSettingStore.fetch(collection);
await useEnvironmentStore().fetch(collection);
await useGlobalReqHeaderStore().fetch(collection);
const collectionStore = useAPICollectionStore();
const result = await collectionStore.get(collection);
if (result) {
headerStore.add({
name: result.name,
route: route.name as string,
});
}
await collectionStore.fetchExpandedFolders(collection);
await collectionStore.fetchTopTreeItems(collection);
await collectionStore.fetchActiveTabs();
if (apiSettingStore.selectedID) {
const data = await getLatestResponse(apiSettingStore.selectedID);
if (data) {
response.value = data;
}
}
window.addEventListener("resize", handleResize);
} catch (err) {
showError(message, err);
} finally {
processing.value = false;
}
});
const updateCollectionColumnWidths = async (params: {
restWidth: number;
value: number;
index: number;
}) => {
const { index, value, restWidth } = params;
if (index < 1 || index > 2) {
return;
}
const widths = settingStore.collectionColumnWidths.slice(0);
// 第一行绝对值,其它记录百分比
const widthIndex = index - 1;
if (index === 1) {
widths[widthIndex] += value;
} else {
widths[widthIndex] += value / restWidth;
if (widths[widthIndex] > 1.0) {
widths[widthIndex] = 0.5;
}
}
try {
await settingStore.updateCollectionColumnWidths(widths);
} catch (err) {
showError(message, err);
}
};
let sendingRequestID = "";
const isCurrentRequest = (reqID: string) => {
return sendingRequestID === reqID;
};
const handleSend = async (id: string) => {
// 中断请求
if (id === abortRequestID) {
sending.value = false;
sendingRequestID = "";
response.value = {} as HTTPResponse;
return;
}
if (sending.value) {
return;
}
const reqID = ulid();
sendingRequestID = reqID;
const { req, originalReq } = apiSettingStore.getHTTPRequestFillValues(id);
try {
response.value = {
status: -1,
} as HTTPResponse;
sending.value = true;
const timeout = settingStore.getRequestTimeout();
const res = await doHTTPRequest({
id,
collection,
req,
originalReq,
timeout,
});
if (isCurrentRequest(reqID)) {
response.value = res;
}
} catch (err) {
if (isCurrentRequest(reqID)) {
response.value = {
api: reqID,
req,
} as HTTPResponse;
showError(message, err);
}
} finally {
if (isCurrentRequest(reqID)) {
sending.value = false;
}
}
};
const offListen = onSelectResponse((resp) => {
response.value = resp;
});
onBeforeUnmount(() => {
stop();
offListen();
usePinRequestStore().$reset();
window.removeEventListener("resize", handleResize);
// 清空选中id
apiSettingStore.select("");
});
return {
response,
sending,
collectionColumnWidths,
processing,
updateCollectionColumnWidths,
handleSend,
maxWidth,
};
},
render() {
const {
processing,
collectionColumnWidths,
updateCollectionColumnWidths,
response,
maxWidth,
} = this;
if (processing) {
return ;
}
let currentWidth = 0;
const widths = collectionColumnWidths.slice(0);
// 最后一个分栏自动适应
if (widths.length) {
widths.push(0);
}
let restWidth = getBodyWidth();
widths.forEach((width) => {
// 绝对值
if (width > 1) {
restWidth = restWidth - width;
}
});
const columns = widths.map((width, index) => {
if (width < 1) {
width = Math.floor(restWidth * width);
}
let element = ;
if (index === 0) {
element = ;
} else if (index === 1) {
element = (
{
return this.handleSend(id);
}}
/>
);
} else if (index === 2) {
element = ;
}
const column = (
{
updateCollectionColumnWidths({
restWidth,
value,
index,
});
}}
>
{element}
);
currentWidth += width;
return column;
});
return {columns}
;
},
});
================================================
FILE: src/views/CookieSetting.tsx
================================================
import { storeToRefs } from "pinia";
import { defineComponent, onBeforeMount, ref } from "vue";
import {
NCard,
useMessage,
DataTableColumns,
NDataTable,
NButton,
NIcon,
NSpace,
useDialog,
NPopconfirm,
} from "naive-ui";
import { css } from "@linaria/core";
import { useCookieStore } from "../stores/cookie";
import { showError, getNormalDialogStyle } from "../helpers/util";
import { i18nCookie, i18nCommon } from "../i18n";
import { CreateOutline, TrashOutline } from "@vicons/ionicons5";
import { Cookie } from "../commands/cookies";
import ExCookieEditor from "../components/ExCookieEditor";
import { margin } from "../constants/style";
enum Mode {
Edit = "edit",
List = "list",
}
const addBtnClass = css`
margin-top: ${margin}px;
width: 100%;
`;
export default defineComponent({
name: "CookieSetting",
setup() {
const message = useMessage();
const dialog = useDialog();
const cookieStore = useCookieStore();
const { cookies } = storeToRefs(cookieStore);
const mode = ref(Mode.List);
const updatedCookie = ref({} as Cookie);
onBeforeMount(async () => {
try {
await cookieStore.fetch();
} catch (err) {
showError(message, err);
}
});
const removeCookie = async (index: number) => {
const cookie = cookieStore.cookies[index];
const d = dialog.warning({
title: i18nCookie("deleteCookie"),
content: i18nCookie("deleteCookieContent"),
positiveText: i18nCommon("confirm"),
onPositiveClick: async () => {
d.loading = true;
try {
await cookieStore.remove(cookie);
} catch (err) {
showError(message, err);
} finally {
d.loading = false;
}
},
});
};
const editCookie = (index: number) => {
const cookie = cookieStore.cookies[index];
updatedCookie.value = cookie;
mode.value = Mode.Edit;
};
const addCookie = () => {
updatedCookie.value = {} as Cookie;
mode.value = Mode.Edit;
};
const clearCookie = async () => {
try {
await cookieStore.clear();
} catch (err) {
showError(message, err);
}
};
return {
mode,
cookies,
removeCookie,
editCookie,
addCookie,
clearCookie,
updatedCookie,
};
},
render() {
const { cookies, removeCookie, editCookie, mode, updatedCookie } = this;
const modalStyle = getNormalDialogStyle(0.8);
let cookieNameMaxWidth = 0;
let domainNameMaxWidth = 0;
const fontWidth = 7;
cookies.forEach((item) => {
const cookieWidth = item.name.length * fontWidth;
const domainWidth = item.domain.length * fontWidth;
if (cookieWidth > cookieNameMaxWidth) {
cookieNameMaxWidth = cookieWidth;
}
if (domainWidth > domainNameMaxWidth) {
domainNameMaxWidth = domainWidth;
}
});
const tablePadding = 10;
const columns: DataTableColumns = [
{
title: i18nCookie("name"),
key: "name",
minWidth: cookieNameMaxWidth + tablePadding,
ellipsis: {
tooltip: true,
},
},
{
title: i18nCookie("value"),
key: "value",
},
{
title: i18nCookie("path"),
key: "path",
width: 60,
},
{
title: i18nCookie("domain"),
key: "domain",
width: domainNameMaxWidth + tablePadding,
},
{
title: i18nCookie("expires"),
key: "expires",
width: 200,
},
{
title: i18nCookie("op"),
key: "op",
width: 120,
render: (row, index) => {
return (
{
editCookie(index);
}}
>
{
removeCookie(index);
}}
>
);
},
},
];
const popConfirmSlots = {
trigger: () => {i18nCookie("clearCookie")},
};
const slots = {
"header-extra": () => (
{
this.clearCookie();
}}
>
{i18nCookie("clearCookieTips")}
),
};
return (
{mode === Mode.List && (
{
this.addCookie();
}}
>
{i18nCommon("add")}
)}
{mode === Mode.Edit && (
{
this.mode = Mode.List;
}}
/>
)}
);
},
});
================================================
FILE: src/views/Dashboard.tsx
================================================
import {
NButton,
NCard,
NDropdown,
NEllipsis,
NGi,
NGrid,
NH3,
NIcon,
NInput,
NText,
useDialog,
useMessage,
} from "naive-ui";
import { defineComponent, onBeforeMount, ref } from "vue";
import { storeToRefs } from "pinia";
import { css } from "@linaria/core";
import {
CreateOutline,
ListOutline,
SearchOutline,
SwapVerticalOutline,
TrashOutline,
} from "@vicons/ionicons5";
import { i18nCommon, i18nDashboard } from "../i18n";
import { padding } from "../constants/style";
import ExDialog from "../components/ExDialog";
import { useAPICollectionStore } from "../stores/api_collection";
import {
APICollection,
newDefaultAPICollection,
} from "../commands/api_collection";
import {
formatSimpleDate,
isMatchTextOrPinYin,
setAppTitle,
showError,
} from "../helpers/util";
import ExLoading from "../components/ExLoading";
import { ExFormItem } from "../components/ExForm";
import { goTo } from "../router";
import { names } from "../router/routes";
import { useHeaderStore } from "../stores/header";
import { useSettingStore } from "../stores/setting";
import { HandleKey } from "../constants/handle_key";
import { appName } from "../constants/common";
const dashboardClass = css`
padding: ${2 * padding}px;
`;
const collecitonDescriptionClass = css`
height: 60px;
`;
const collectionContentClass = css`
cursor: pointer;
`;
const sortOptionsClass = css`
width: 100%;
.option {
margin: 0 10px;
}
`;
enum SortType {
LastModified = "lastModified",
NameAsc = "nameAsc",
NameDesc = "nameDesc",
OldestFirst = "olderFirst",
NewestFirst = "newestFirst",
}
const getFormItems = (): ExFormItem[] => {
return [
{
key: "name",
label: i18nCommon("name"),
placeholer: i18nCommon("namePlaceholder"),
rule: {
required: true,
message: i18nCommon("nameRequireError"),
trigger: "blur",
},
},
{
key: "description",
label: i18nCommon("description"),
placeholer: i18nCommon("descriptionPlaceholder"),
inputType: "textarea",
},
];
};
// 过滤与排序
function filterAndSort(
apiCollections: APICollection[],
keyword: string,
sortOrder: string,
): APICollection[] {
const collections = apiCollections.filter((item) => {
if (!keyword) {
return true;
}
const arr: string[] = [
item.name.toLowerCase(),
item.description.toLowerCase(),
];
return isMatchTextOrPinYin(arr.join(""), keyword);
});
collections.sort((col1, col2) => {
let value1 = "";
let value2 = "";
switch (sortOrder) {
case SortType.NameAsc:
value1 = col1.name;
value2 = col2.name;
break;
case SortType.NameDesc:
value2 = col1.name;
value1 = col2.name;
break;
case SortType.OldestFirst:
value1 = col1.createdAt;
value2 = col2.createdAt;
break;
case SortType.NewestFirst:
value2 = col1.createdAt;
value1 = col2.createdAt;
break;
default:
value2 = col1.updatedAt;
value1 = col2.updatedAt;
break;
}
if (value1 > value2) {
return 1;
}
if (value1 < value2) {
return -1;
}
return 0;
});
return collections;
}
export default defineComponent({
name: "DashboardView",
setup() {
const message = useMessage();
const dialog = useDialog();
const store = useAPICollectionStore();
const settingStore = useSettingStore();
const { apiCollections, fetching } = storeToRefs(store);
useHeaderStore().clear();
const keyword = ref("");
const sortOrder = ref(
settingStore.collectionSortType || SortType.LastModified,
);
const createCollection = () => {
ExDialog({
dialog,
title: i18nDashboard("newCollection"),
formItems: getFormItems(),
enterTriggerSubmit: true,
onConfirm: async (data) => {
try {
const collection = newDefaultAPICollection();
Object.assign(collection, data);
await store.add(collection);
} catch (err) {
showError(message, err);
}
},
});
};
onBeforeMount(async () => {
try {
await setAppTitle(appName);
await store.fetch();
} catch (err) {
showError(message, err);
}
});
const handleCollection = (collection: APICollection, key: string) => {
switch (key) {
case HandleKey.Modify:
{
const formItems = getFormItems();
formItems.forEach((item) => {
const value = collection[item.key];
if (value) {
item.defaultValue = value as string;
}
});
ExDialog({
dialog,
title: i18nDashboard("updateCollection"),
formItems,
onConfirm: async (data) => {
try {
await store.update(Object.assign(collection, data));
} catch (err) {
showError(message, err);
}
},
});
}
break;
case HandleKey.Delete:
{
const d = dialog.warning({
title: i18nDashboard("deleteCollection"),
content: i18nDashboard("deleteCollectionContent"),
positiveText: i18nCommon("confirm"),
onPositiveClick: async () => {
d.loading = true;
try {
await store.remove(collection.id);
message.success(i18nDashboard("deleteCollectionDone"));
} catch (err) {
showError(message, err);
} finally {
d.loading = false;
}
},
});
}
break;
}
};
const changeSortType = async (value: string) => {
sortOrder.value = value;
try {
await settingStore.updateCollectionSortType(value);
} catch (err) {
// 此出错不展示
console.error(err);
}
};
return {
sortOrder,
changeSortType,
keyword,
createCollection,
fetching,
apiCollections,
handleCollection,
};
},
render() {
const {
sortOrder,
createCollection,
fetching,
apiCollections,
handleCollection,
keyword,
} = this;
const sortOptions = [
{
label: i18nDashboard("sortLastModified"),
key: SortType.LastModified,
},
{
label: i18nDashboard("sortNameAsc"),
key: SortType.NameAsc,
},
{
label: i18nDashboard("sortNameDesc"),
key: SortType.NameDesc,
},
{
label: i18nDashboard("sortOlderFirst"),
key: SortType.OldestFirst,
},
{
label: i18nDashboard("sortNewestFirst"),
key: SortType.NewestFirst,
},
];
const slots = {
suffix: () => (
),
};
const header = (
{i18nCommon("dashboard")}
{
this.keyword = value;
}}
placeholder={i18nCommon("keywordFilterPlaceholder")}
/>
{
return {option.label};
}}
>
{
createCollection();
}}
>
{i18nCommon("create")}
);
let collections = ;
if (!fetching) {
const options = [
{
label: i18nCommon("modify"),
key: HandleKey.Modify,
icon: () => (
),
},
{
label: i18nCommon("delete"),
key: HandleKey.Delete,
icon: () => (
),
},
];
const items = filterAndSort(apiCollections, keyword, sortOrder);
const arr = items.map((item) => {
const slots = {
"header-extra": () => (
{
handleCollection(item, key);
}}
>
{
e.stopPropagation();
}}
>
),
};
return (
{
goTo(names.collection, {
query: {
collection: item.id,
},
});
}}
>
{item.description}
{formatSimpleDate(item.updatedAt)}
);
});
collections = (
{arr}
);
}
return (
);
},
});
================================================
FILE: src/views/StoreSetting.tsx
================================================
import { css } from "@linaria/core";
import { TrashOutline } from "@vicons/ionicons5";
import {
DataTableColumns,
NButton,
NCard,
NDataTable,
NIcon,
NPopconfirm,
useMessage,
} from "naive-ui";
import { defineComponent } from "vue";
import { reload, showError } from "../helpers/util";
import { i18nStore } from "../i18n";
import { clearStore, StoreKey } from "../stores/local";
const storeClass = css`
max-width: 800px;
.n-card__content {
min-height: 200px;
max-height: 400px;
overflow-y: auto;
}
`;
export default defineComponent({
name: "StoreSetting",
setup() {
const message = useMessage();
const stores = [
{
name: i18nStore("settingStore"),
desc: i18nStore("settingStoreDesc"),
id: StoreKey.setting,
},
{
name: i18nStore("pinRequestsStore"),
desc: i18nStore("pinRequestsStoreDesc"),
id: StoreKey.pinRequests,
},
{
name: i18nStore("latestResponseStore"),
desc: i18nStore("latestResponseStoreDesc"),
id: StoreKey.latestResponse,
},
];
const clear = async (index: number) => {
try {
const { id } = stores[index];
await clearStore(id);
message.info(i18nStore("clearSuccess"));
setTimeout(() => {
reload();
}, 3000);
} catch (err) {
showError(message, err);
}
};
return {
stores,
clear,
};
},
render() {
const { stores } = this;
const columns: DataTableColumns = [
{
title: i18nStore("name"),
key: "name",
},
{
title: i18nStore("desc"),
key: "desc",
},
{
title: i18nStore("op"),
key: "op",
width: 120,
render: (row, index) => {
const slots = {
trigger: () => (
),
};
const name = stores[index].name;
const tips = i18nStore("clearTips").replace("%s", name);
return (
{
this.clear(index);
}}
>
{tips}
);
},
},
];
return (
);
},
});
================================================
FILE: src/views/VariableSetting.tsx
================================================
import { NCard, NP, useMessage } from "naive-ui";
import { defineComponent, onBeforeMount } from "vue";
import { css } from "@linaria/core";
import ExKeyValue, {
HandleOption,
HandleOptionCategory,
} from "../components/ExKeyValue";
import { KVParam } from "../commands/interface";
import { useRoute } from "vue-router";
import { useEnvironmentStore } from "../stores/environment";
import { useCustomizeStore } from "../stores/variable";
import { showError } from "../helpers/util";
import { storeToRefs } from "pinia";
import ExLoading from "../components/ExLoading";
import {
newDefaultVariable,
Variable,
VariableCategory,
VariableStatus,
} from "../commands/variable";
import { useGlobalReqHeaderStore } from "../stores/global_req_header";
const variableClass = css`
.n-card__content {
min-height: 200px;
max-height: 400px;
overflow-y: auto;
}
`;
function convertKVParams(variables: Variable[]): KVParam[] {
return variables.map((item) => {
return {
key: item.name,
value: item.value,
enabled: item.enabled == VariableStatus.Enabled,
};
});
}
export default defineComponent({
name: "VariableSetting",
props: {
category: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
tips: {
type: String,
required: true,
},
maxWidth: {
type: Number,
default: () => 800,
},
},
setup(props) {
const message = useMessage();
const route = useRoute();
const collection = route.query.collection as string;
let variableStore = useCustomizeStore();
switch (props.category) {
case VariableCategory.Environment:
variableStore = useEnvironmentStore();
break;
case VariableCategory.GlobalReqHeaders:
variableStore = useGlobalReqHeaderStore();
break;
default:
break;
}
const { fetching, variables } = storeToRefs(variableStore);
onBeforeMount(async () => {
try {
await variableStore.fetch(collection);
} catch (err) {
showError(message, err);
}
});
const handle = async (opt: HandleOption) => {
switch (opt.category) {
case HandleOptionCategory.Add:
{
const item = opt.param;
let enabled = VariableStatus.Enabled;
if (item && !item.enabled) {
enabled = VariableStatus.Disabled;
}
const value = newDefaultVariable();
value.collection = collection;
value.name = item?.key || "";
value.value = item?.value || "";
value.enabled = enabled;
await variableStore.add(value);
}
break;
case HandleOptionCategory.Delete:
{
if (opt.index < variableStore.variables.length) {
const item = variableStore.variables[opt.index];
await variableStore.remove(item.id);
}
}
break;
default:
{
if (opt.index < variableStore.variables.length) {
const updateItem = variableStore.variables[opt.index];
const item = opt.param;
let enabled = VariableStatus.Enabled;
if (item && !item.enabled) {
enabled = VariableStatus.Disabled;
}
updateItem.name = item?.key || "";
updateItem.value = item?.value || "";
updateItem.enabled = enabled;
await variableStore.update(updateItem);
}
}
break;
}
};
const handleUpdate = async (params: KVParam[]) => {
const arr = variableStore.variables.slice(0);
const promiseList = [] as Promise[];
params.forEach((item, index) => {
const enabled = item.enabled
? VariableStatus.Enabled
: VariableStatus.Disabled;
const value = arr[index];
// 增加元素
if (!value) {
const newValue = newDefaultVariable();
newValue.collection = collection;
newValue.name = item.key;
newValue.value = item.value;
newValue.enabled = enabled;
promiseList.push(variableStore.add(newValue));
return;
}
// 其中一个不一样
if (
value.name !== item.key ||
value.value !== item.value ||
value.enabled !== enabled
) {
value.name = item.key;
value.value = item.value;
value.enabled = enabled;
promiseList.push(variableStore.update(value));
}
});
await Promise.all(promiseList);
};
return {
fetching,
variables,
handle,
handleUpdate,
};
},
render() {
const { title, tips, maxWidth } = this.$props;
const { variables, fetching } = this;
if (fetching) {
return ;
}
return (
{tips}
);
},
});
================================================
FILE: src-tauri/.gitignore
================================================
# Generated by Cargo
# will have compiled files and executables
/target/
================================================
FILE: src-tauri/Cargo.toml
================================================
[package]
name = "cyberapi"
version = "0.1.21"
description = "API tool based on tauri, it is smaller and faster."
authors = ["tree.xie@outlook.com"]
license = "Apache License 2.0"
repository = "https://github.com/vicanso/cyberapi"
default-run = "cyberapi"
edition = "2021"
rust-version = "1.64"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.5.0", features = [] }
[dependencies]
base64 = "0.21.5"
brotli-decompressor = "2.5.1"
chrono = "0.4.31"
cookie = "0.18.0"
cookie_store = "0.20.0"
hyper = { version = "0.14.28", features = ["client", "http1"] }
hyper-rustls = "0.24.1"
hyper-timeout = "0.4.1"
libflate = "2.0.0"
once_cell = "1.18.0"
sea-orm = { version = "0.12.10", features = [
"sqlx-sqlite",
"runtime-tokio-rustls",
"with-json",
] }
serde = { version = "1.0.195", features = ["derive"] }
serde_json = "1.0.111"
strum = "0.25.0"
strum_macros = "0.25.1"
tauri = { version = "1.6.7", features = [
"app-hide",
"clipboard-all",
"dialog-all",
"fs-exists",
"fs-read-dir",
"fs-read-file",
"fs-write-file",
"os-all",
"path-all",
"process-all",
"window-all",
] }
time = "0.3.31"
tokio = { version = "1.37.0", features = ["parking_lot"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
url = "2.4.0"
windows = "0.48.0"
zip = { version = "0.6.4", default-features = false, features = ["deflate"] }
[features]
# by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
default = ["custom-protocol"]
# this feature is used used for production builds where `devPath` points to the filesystem
# DO NOT remove this
custom-protocol = ["tauri/custom-protocol"]
[profile.release]
# Strip expensive panic clean-up logic
panic = "abort"
# Compile crates one after another so the compiler can optimize better
codegen-units = 1
strip = true
# 如果设置为true会导致short cut不可用
lto = "thin"
opt-level = "z"
================================================
FILE: src-tauri/build.rs
================================================
fn main() {
tauri_build::build()
}
================================================
FILE: src-tauri/src/commands.rs
================================================
use crate::error::CyberAPIError;
use crate::schemas::{self, APICollection, APIFolder, APISetting, Variable};
use crate::{cookies, http_request};
use tauri::Manager;
use tauri::{command, Window};
pub type CommandResult = Result;
// 关闭启动视窗切换至主视窗
#[command]
pub fn close_splashscreen(window: Window) {
// 关闭启动视图
if let Some(splashscreen) = window.get_window("splashscreen") {
splashscreen.close().unwrap();
}
// 展示主视图
window.get_window("main").unwrap().show().unwrap();
}
// 新增API配置
#[command(async)]
pub async fn add_api_setting(setting: APISetting) -> CommandResult {
let result = schemas::add_api_setting(setting).await?;
Ok(result)
}
// 更新API配置
#[command(async)]
pub async fn update_api_setting(setting: APISetting) -> CommandResult {
let result = schemas::update_api_setting(setting).await?;
Ok(result)
}
// 初始化数据库
#[command(async)]
pub async fn init_tables() -> CommandResult<()> {
schemas::init_tables().await?;
Ok(())
}
#[command(async)]
pub async fn export_tables() -> CommandResult {
let filename = schemas::export_tables().await?;
Ok(filename)
}
#[command(async)]
pub async fn import_tables(file: String) -> CommandResult<()> {
schemas::import_tables(file).await?;
Ok(())
}
// 获取所有API配置
#[command(async)]
pub async fn list_api_setting(collection: String) -> CommandResult> {
let result = schemas::list_api_setting(collection).await?;
Ok(result)
}
// 删除API配置
#[command(async)]
pub async fn delete_api_settings(ids: Vec) -> CommandResult<()> {
schemas::delete_api_settings(ids).await?;
Ok(())
}
// 新增collection
#[command(async)]
pub async fn add_api_collection(collection: APICollection) -> CommandResult {
let result = schemas::add_api_collection(collection).await?;
Ok(result)
}
// 更新collection
#[command(async)]
pub async fn update_api_collection(collection: APICollection) -> CommandResult {
let result = schemas::update_api_collection(collection).await?;
Ok(result)
}
// 获取所有collection
#[command(async)]
pub async fn list_api_collection() -> CommandResult> {
let result = schemas::list_api_collection().await?;
Ok(result)
}
#[command(async)]
pub async fn delete_api_collection(id: String) -> CommandResult {
schemas::delete_api_setting_by_collection(id.clone()).await?;
schemas::delete_api_folder_by_collection(id.clone()).await?;
let count = schemas::delete_api_collection(id).await?;
Ok(count)
}
// 新增API目录
#[command(async)]
pub async fn add_api_folder(folder: APIFolder) -> CommandResult {
let result = schemas::add_api_folder(folder).await?;
Ok(result)
}
// 更新API目录
#[command(async)]
pub async fn update_api_folder(folder: APIFolder) -> CommandResult {
let result = schemas::update_api_folder(folder).await?;
Ok(result)
}
// 获取所有API目录
#[command(async)]
pub async fn list_api_folder(collection: String) -> CommandResult> {
let result = schemas::list_api_folder(collection).await?;
Ok(result)
}
// 删除API目录对应的所有子目录
#[command(async)]
pub async fn delete_api_folder(id: String) -> CommandResult {
let mut result = schemas::list_api_folder_all_children(id.clone()).await?;
result.folders.push(id);
schemas::delete_api_folders(result.folders.clone()).await?;
schemas::delete_api_settings(result.settings.clone()).await?;
Ok(result)
}
// 新增变量
#[command(async)]
pub async fn add_variable(value: Variable) -> CommandResult {
let result = schemas::add_variable(value).await?;
Ok(result)
}
// 更新变量
#[command(async)]
pub async fn update_variable(value: Variable) -> CommandResult {
let result = schemas::update_variable(value).await?;
Ok(result)
}
// 删除变量
#[command(async)]
pub async fn delete_variable(ids: Vec) -> CommandResult {
let count = schemas::delete_variable(ids).await?;
Ok(count)
}
// 获取所有变量
#[command(async)]
pub async fn list_variable(collection: String, category: String) -> CommandResult> {
let result = schemas::list_variable(collection, category).await?;
Ok(result)
}
// 执行HTTP请求
#[command(async)]
pub async fn do_http_request(
api: String,
req: http_request::HTTPRequest,
timeout: http_request::RequestTimeout,
) -> CommandResult {
http_request::request(api, req, timeout).await
}
// 获取所有cookie
#[command(async)]
pub fn list_cookie() -> CommandResult> {
cookies::list_cookie()
}
// 删除cookie
#[command(async)]
pub fn delete_cookie(c: cookies::Cookie) -> CommandResult<()> {
cookies::delete_cookie_from_store(c)?;
Ok(())
}
// 添加cookie
#[command(async)]
pub fn add_cookie(c: cookies::Cookie) -> CommandResult<()> {
cookies::add_cookie(c)?;
Ok(())
}
// 清除cookie
#[command(async)]
pub fn clear_cookie() -> CommandResult<()> {
cookies::clear_cookie_from_store()?;
Ok(())
}
// 获取最新版本
#[command(async)]
pub async fn get_latest_version() -> CommandResult {
let result = schemas::get_latest_version().await?;
Ok(result)
}
// 添加版本记录
#[command(async)]
pub async fn add_version(version: schemas::Version) -> CommandResult<()> {
schemas::add_version(version).await?;
Ok(())
}
================================================
FILE: src-tauri/src/cookies.rs
================================================
use cookie_store::CookieStore;
use once_cell::sync::OnceCell;
use serde::{Deserialize, Serialize};
use std::{
fs::File, fs::OpenOptions, io::BufReader, io::BufWriter, path::Path, sync::Mutex,
sync::MutexGuard,
};
use url::Url;
use crate::error::CyberAPIError;
use crate::util::get_app_dir;
static COOKIE_STORE: OnceCell> = OnceCell::new();
fn init_store() -> &'static Mutex {
COOKIE_STORE.get_or_init(|| {
let filename = Path::new(get_app_dir()).join(COOKIE_FILE);
let file = OpenOptions::new()
.create(true)
.write(true)
.read(true)
.open(filename)
.map(BufReader::new)
.unwrap();
let store = cookie_store::CookieStore::load_json(file).unwrap();
Mutex::new(store)
})
}
const COOKIE_FILE: &str = "cookies.json";
#[derive(Debug, Serialize, Deserialize)]
pub struct Cookie {
name: String,
value: String,
path: String,
domain: String,
expires: String,
}
impl Cookie {
fn to_set_cookie_string(&self) -> String {
let mut arr = Vec::new();
arr.push(format!("{}={}", self.name, self.value));
if !self.path.is_empty() {
arr.push(format!("Path={}", self.path));
}
if !self.domain.is_empty() {
arr.push(format!("Domain={}", self.domain));
}
if !self.expires.is_empty() {
arr.push(format!("Expires={}", self.expires));
}
arr.join(";")
}
fn get_url(&self) -> String {
let mut path = self.path.clone();
if path.is_empty() {
path = "/".to_string()
}
format!("http://{}{}", self.domain, path)
}
}
pub fn get_cookie_store() -> MutexGuard<'static, CookieStore> {
let result = init_store();
result.lock().unwrap()
}
fn save_store(store: MutexGuard) -> Result<(), CyberAPIError> {
let filename = Path::new(get_app_dir()).join(COOKIE_FILE);
let mut writer = File::create(filename).map(BufWriter::new)?;
store.save_json(&mut writer)?;
Ok(())
}
pub fn delete_cookie_from_store(c: Cookie) -> Result<(), CyberAPIError> {
let name = c.name;
if name.is_empty() {
return Ok(());
}
let mut store = get_cookie_store();
let domain = c.domain.as_str();
let path = c.path.as_str();
store.remove(domain, path, name.as_str());
save_store(store)?;
Ok(())
}
pub fn clear_cookie_from_store() -> Result<(), CyberAPIError> {
let mut store = get_cookie_store();
store.clear();
save_store(store)?;
Ok(())
}
pub fn save_cookie_store(set_cookies: Vec, current_url: &Url) -> Result<(), CyberAPIError> {
let mut store = get_cookie_store();
let now = chrono::Local::now().timestamp();
for ele in set_cookies {
let c = cookie::Cookie::parse(&ele)?;
let mut expired = false;
if let Some(expires) = c.expires() {
if let Some(expired_time) = expires.datetime() {
expired = expired_time.unix_timestamp() < now;
}
}
if expired {
store.remove(
c.domain().unwrap_or_default(),
c.path().unwrap_or_default(),
c.name(),
);
} else {
store.parse(ele.as_str(), current_url)?;
}
}
save_store(store)?;
Ok(())
}
pub fn list_cookie() -> Result, CyberAPIError> {
let store = get_cookie_store();
let mut result: Vec = Vec::new();
for ele in store.iter_any() {
let data = serde_json::to_string(ele)?;
if !data.is_empty() {
result.push(data);
}
}
Ok(result)
}
pub fn add_cookie(c: Cookie) -> Result<(), CyberAPIError> {
let mut store = get_cookie_store();
let url = c.get_url();
let request_url = Url::parse(&url)?;
let cookie_str = c.to_set_cookie_string();
store.parse(&cookie_str, &request_url)?;
save_store(store)?;
Ok(())
}
================================================
FILE: src-tauri/src/entities/api_collections.rs
================================================
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "api_collections")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub name: Option,
pub description: Option,
pub created_at: Option,
pub updated_at: Option,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
================================================
FILE: src-tauri/src/entities/api_folders.rs
================================================
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "api_folders")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub collection: String,
pub children: Option,
pub name: Option,
pub created_at: Option,
pub updated_at: Option,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
================================================
FILE: src-tauri/src/entities/api_settings.rs
================================================
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "api_settings")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub collection: String,
pub name: Option,
pub category: Option,
pub setting: Option,
pub created_at: Option,
pub updated_at: Option,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
================================================
FILE: src-tauri/src/entities/mod.rs
================================================
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2
pub mod prelude;
pub mod api_collections;
pub mod api_folders;
pub mod api_settings;
pub mod variables;
pub mod versions;
================================================
FILE: src-tauri/src/entities/prelude.rs
================================================
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2
pub use super::api_collections::Entity as ApiCollections;
pub use super::api_folders::Entity as ApiFolders;
pub use super::api_settings::Entity as ApiSettings;
pub use super::variables::Entity as Variables;
pub use super::versions::Entity as Versions;
================================================
FILE: src-tauri/src/entities/variables.rs
================================================
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "variables")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub category: String,
pub collection: String,
pub name: Option,
pub value: Option,
pub enabled: Option,
pub created_at: Option,
pub updated_at: Option,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
================================================
FILE: src-tauri/src/entities/versions.rs
================================================
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.2
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "versions")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub version: String,
pub created_at: Option,
pub updated_at: Option,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
================================================
FILE: src-tauri/src/error.rs
================================================
use serde::Serialize;
use zip::result::ZipError;
#[derive(Debug, Clone, Serialize)]
pub struct CyberAPIError {
message: String,
category: String,
}
impl From for CyberAPIError {
fn from(error: sea_orm::DbErr) -> Self {
CyberAPIError {
message: error.to_string(),
category: "seaOrm".to_string(),
}
}
}
impl From for CyberAPIError {
fn from(error: hyper::http::Error) -> Self {
CyberAPIError {
message: error.to_string(),
category: "http".to_string(),
}
}
}
impl From for CyberAPIError {
fn from(error: hyper::Error) -> Self {
CyberAPIError {
message: error.to_string(),
category: "http".to_string(),
}
}
}
impl From for CyberAPIError {
fn from(error: tauri::http::InvalidUri) -> Self {
CyberAPIError {
message: error.to_string(),
category: "invalidUri".to_string(),
}
}
}
impl From for CyberAPIError {
fn from(error: hyper::header::InvalidHeaderValue) -> Self {
CyberAPIError {
message: error.to_string(),
category: "invalidHeader".to_string(),
}
}
}
impl From for CyberAPIError {
fn from(error: hyper::header::InvalidHeaderName) -> Self {
CyberAPIError {
message: error.to_string(),
category: "invalidHeaderName".to_string(),
}
}
}
impl From for CyberAPIError {
fn from(error: hyper::header::ToStrError) -> Self {
CyberAPIError {
message: error.to_string(),
category: "toStrError".to_string(),
}
}
}
impl From for CyberAPIError {
fn from(error: std::io::Error) -> Self {
CyberAPIError {
message: error.to_string(),
category: "io".to_string(),
}
}
}
impl From for CyberAPIError {
fn from(error: cookie_store::Error) -> Self {
CyberAPIError {
message: error.to_string(),
category: "cookieStore".to_string(),
}
}
}
impl From for CyberAPIError {
fn from(error: url::ParseError) -> Self {
CyberAPIError {
message: error.to_string(),
category: "urlParse".to_string(),
}
}
}
impl From for CyberAPIError {
fn from(error: cookie_store::CookieError) -> Self {
CyberAPIError {
message: error.to_string(),
category: "cookieStore".to_string(),
}
}
}
impl From for CyberAPIError {
fn from(error: serde_json::Error) -> Self {
CyberAPIError {
message: error.to_string(),
category: "serdeJson".to_string(),
}
}
}
impl From for CyberAPIError {
fn from(error: base64::DecodeError) -> Self {
CyberAPIError {
message: error.to_string(),
category: "base64".to_string(),
}
}
}
impl From for CyberAPIError {
fn from(error: ZipError) -> Self {
CyberAPIError {
message: error.to_string(),
category: "zip".to_string(),
}
}
}
impl From for CyberAPIError {
fn from(error: cookie::ParseError) -> Self {
CyberAPIError {
message: error.to_string(),
category: "cookie".to_string(),
}
}
}
================================================
FILE: src-tauri/src/http_request.rs
================================================
use crate::cookies;
use crate::error::CyberAPIError;
use base64::{engine::general_purpose, Engine as _};
use hyper::{
body::{Buf, Bytes},
client::connect::HttpInfo,
client::HttpConnector,
header::{HeaderName, HeaderValue},
Body, Client, Method, Request, Uri,
};
use hyper_rustls::HttpsConnectorBuilder;
use hyper_timeout::TimeoutConnector;
use libflate::gzip::Decoder;
use once_cell::sync::OnceCell;
use tracing_subscriber::Layer;
use serde::{Deserialize, Serialize};
use std::{
collections::BTreeMap,
collections::HashMap,
io::Read,
sync::atomic::{AtomicBool, AtomicU64, Ordering},
sync::Mutex,
time::Duration,
vec,
};
use url::Url;
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct HTTPRequestKVParam {
pub key: String,
pub value: String,
pub enabled: bool,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct HTTPRequest {
pub method: String,
pub uri: String,
pub body: String,
pub content_type: String,
pub headers: Vec,
pub query: Vec,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct RequestTimeout {
pub connect: u64,
pub write: u64,
pub read: u64,
}
#[derive(Deserialize, Serialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct HTTPStats {
pub remote_addr: String,
pub is_https: bool,
pub cipher: String,
pub dns_lookup: u32,
pub tcp: u32,
pub tls: u32,
pub send: u32,
pub server_processing: u32,
pub content_transfer: u32,
pub total: u32,
}
impl HTTPStats {
fn new() -> Self {
HTTPStats {
..Default::default()
}
}
}
impl From<&HTTPTrace> for HTTPStats {
fn from(trace: &HTTPTrace) -> Self {
let mut stats = HTTPStats::new();
stats.is_https = trace.is_tls();
stats.cipher = trace.get_cipher();
stats.dns_lookup = trace.dns_consuming();
stats.tcp = trace.tcp_consuming();
stats.tls = trace.tls_consuming();
stats.server_processing = trace.server_processing_consuming();
stats.send = trace.send_consuming();
stats.content_transfer = trace.content_transfer_consuming();
stats.total = trace.consuming();
stats
}
}
#[derive(Default)]
struct HTTPTrace {
is_tls_value: AtomicBool,
cipher_value: Mutex,
start_value: AtomicU64,
get_conn_value: AtomicU64,
dns_start_value: AtomicU64,
dns_done_value: AtomicU64,
tcp_start_value: AtomicU64,
tcp_done_value: AtomicU64,
tls_start_value: AtomicU64,
tls_done_value: AtomicU64,
http_start_value: AtomicU64,
written_value: AtomicU64,
got_first_response_byte_value: AtomicU64,
done_value: AtomicU64,
}
// get conn from pool
// http connect start
// dns start
// dns done
// conn start
// tcp start
// tcp done
// tls start
// tls done
// http start
// first byte
// done
impl HTTPTrace {
fn now(&self) -> u64 {
chrono::Utc::now().timestamp_millis() as u64
}
fn new() -> Self {
HTTPTrace {
..Default::default()
}
}
fn reset(&self) {
self.set_cipher("".to_string());
self.is_tls_value.store(false, Ordering::Relaxed);
self.start_value.store(0, Ordering::Relaxed);
self.get_conn_value.store(0, Ordering::Relaxed);
self.dns_start_value.store(0, Ordering::Relaxed);
self.dns_done_value.store(0, Ordering::Relaxed);
self.tcp_start_value.store(0, Ordering::Relaxed);
self.tcp_done_value.store(0, Ordering::Relaxed);
self.tls_start_value.store(0, Ordering::Relaxed);
self.tls_done_value.store(0, Ordering::Relaxed);
self.http_start_value.store(0, Ordering::Relaxed);
self.written_value.store(0, Ordering::Relaxed);
self.got_first_response_byte_value
.store(0, Ordering::Relaxed);
self.done_value.store(0, Ordering::Relaxed);
}
fn set_cipher(&self, value: String) {
if let Ok(mut cipher) = self.cipher_value.lock() {
*cipher = value;
}
// let mut cipher = value;
// let value = self.cipher_value.lock();
// value.
// self.cipher_value.store(&mut cipher, Ordering::Relaxed);
}
fn get_cipher(&self) -> String {
if let Ok(cipher) = self.cipher_value.lock() {
return cipher.to_string();
}
"".to_string()
// let value = self.cipher_value.load(Ordering::Relaxed);
// if value.is_null() {
// return "".to_string();
// }
// // value.
// format!("{:?}", value.as_str())
// value.to_owned().to_string()
// return "".to_string();
// return value.into();
}
fn is_tls(&self) -> bool {
self.is_tls_value.load(Ordering::Relaxed)
}
fn tls(&self) {
self.is_tls_value.store(true, Ordering::Relaxed);
}
fn get_conn_from_pool(&self) {
self.start_value.store(self.now(), Ordering::Relaxed)
}
fn get_conn(&self) {
self.get_conn_value.store(self.now(), Ordering::Relaxed)
}
fn dns_start(&self) {
self.dns_start_value.store(self.now(), Ordering::Relaxed)
}
fn dns_done(&self) {
self.dns_done_value.store(self.now(), Ordering::Relaxed);
}
fn tcp_start(&self) {
self.tcp_start_value.store(self.now(), Ordering::Relaxed);
}
fn tcp_done(&self) {
self.tcp_done_value.store(self.now(), Ordering::Relaxed);
}
fn tls_start(&self) {
self.tls_start_value.store(self.now(), Ordering::Relaxed);
}
fn tls_done(&self) {
self.tls_done_value.store(self.now(), Ordering::Relaxed);
}
fn http_start(&self) {
self.http_start_value.store(self.now(), Ordering::Relaxed);
}
fn got_first_response_byte(&self) {
self.got_first_response_byte_value
.store(self.now(), Ordering::Relaxed);
}
fn written(&self) {
self.written_value.store(self.now(), Ordering::Relaxed);
}
fn done(&self) {
self.done_value.store(self.now(), Ordering::Relaxed);
}
fn send_consuming(&self) -> u32 {
let http_start_value = self.http_start_value.load(Ordering::Relaxed);
let written_value = self.written_value.load(Ordering::Relaxed);
if http_start_value == 0 || written_value == 0 {
return 0;
}
(written_value - http_start_value) as u32
}
fn dns_consuming(&self) -> u32 {
let dns_start_value = self.dns_start_value.load(Ordering::Relaxed);
let dns_done_value = self.dns_done_value.load(Ordering::Relaxed);
if dns_start_value == 0 || dns_done_value == 0 {
return 0;
}
(dns_done_value - dns_start_value) as u32
}
fn tcp_consuming(&self) -> u32 {
let tcp_start_value = self.tcp_start_value.load(Ordering::Relaxed);
let tcp_done_value = self.tcp_done_value.load(Ordering::Relaxed);
if tcp_start_value == 0 || tcp_done_value == 0 {
return 0;
}
(tcp_done_value - tcp_start_value) as u32
}
fn tls_consuming(&self) -> u32 {
let tls_start_value = self.tls_start_value.load(Ordering::Relaxed);
let tls_done_value = self.tls_done_value.load(Ordering::Relaxed);
if tls_start_value == 0 || tls_done_value == 0 {
return 0;
}
(tls_done_value - tls_start_value) as u32
}
fn server_processing_consuming(&self) -> u32 {
let written_value = self.written_value.load(Ordering::Relaxed);
let got_first_response_byte_value =
self.got_first_response_byte_value.load(Ordering::Relaxed);
if written_value == 0 || got_first_response_byte_value == 0 {
return 0;
}
(got_first_response_byte_value - written_value) as u32
}
fn content_transfer_consuming(&self) -> u32 {
let got_first_response_byte_value =
self.got_first_response_byte_value.load(Ordering::Relaxed);
let done_value = self.done_value.load(Ordering::Relaxed);
if got_first_response_byte_value == 0 || done_value == 0 {
return 0;
}
(done_value - got_first_response_byte_value) as u32
}
fn consuming(&self) -> u32 {
let start_value = self.start_value.load(Ordering::Relaxed);
let done_value = self.done_value.load(Ordering::Relaxed);
if start_value == 0 || done_value == 0 {
return 0;
}
(done_value - start_value) as u32
}
}
static HTTP_TRACE: OnceCell = OnceCell::new();
fn get_http_trace() -> &'static HTTPTrace {
HTTP_TRACE.get_or_init(HTTPTrace::new)
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct HTTPResponse {
pub api: String,
pub latency: u32,
pub status: u16,
pub headers: HashMap>,
pub body: String,
pub stats: HTTPStats,
pub body_size: u32,
}
struct JsonVisitor<'a>(&'a mut BTreeMap);
impl<'a> tracing::field::Visit for JsonVisitor<'a> {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
self.0
.insert(field.name().to_string(), format!("{:?}", value));
}
}
pub struct HTTPTraceLayer;
impl Layer for HTTPTraceLayer
where
S: tracing::Subscriber,
// Scary! But there's no need to even understand it. We just need it.
S: for<'lookup> tracing_subscriber::registry::LookupSpan<'lookup>,
{
fn on_event(&self, event: &tracing::Event<'_>, _: tracing_subscriber::layer::Context<'_, S>) {
let trace = get_http_trace();
let target = event.metadata().target();
if !target.starts_with("hyper::") && !trace.is_tls() {
return;
}
let mut fields = BTreeMap::new();
let mut visitor = JsonVisitor(&mut fields);
event.record(&mut visitor);
let message = fields.get("message");
if message.is_none() {
return;
}
let message = message.unwrap();
// 暂时不会使用tracing span,使用比较简陋的处理方法
if trace.is_tls() {
// tls 开始: Sending ClientHello Message
// tls算法: Using ciphersuite TLS13_AES_256_GCM_SHA384
// 获取到的服务器证书 Server cert is
// 开始http client handshake Http1
if message.starts_with("Sending ClientHello Message") {
trace.tls_start();
} else if message.starts_with("Server cert is") {
trace.tls_done();
} else if message.starts_with("Using ciphersuite ") {
let cipher = message.replace("Using ciphersuite ", "");
trace.set_cipher(cipher);
// let p = AtomicPtr::new(&mut cipher);
// trace.cipher.store(&mut cipher, Ordering::Relaxed);
}
}
match target {
"hyper::client::pool" => {
// 从连接池中获取
if message.contains("checkout waiting for idle connection") {
trace.get_conn_from_pool();
}
}
"hyper::client::connect::http" => {
// HTTP 开始连接
if message.starts_with("Http::connect;") {
trace.get_conn();
} else if message.starts_with("connecting to") {
trace.dns_done();
trace.tcp_start();
// 开始TCP连接
} else if message.starts_with("connected to") {
// TCP连接成功
trace.tcp_done();
}
}
"hyper::client::connect::dns" => {
if message.starts_with("resolving host") {
trace.dns_start();
}
}
"hyper::client::conn" => {
// 开始连接
if message.starts_with("client handshake") {
// http开始
trace.http_start();
}
}
"hyper::client::client" => {
// 如果是https,包括tls
if message.starts_with("handshake complete") {
// http 请求完成,开始发送数据
trace.written();
}
}
"hyper::proto::h1::io" => {
// 获取首字节
if message.starts_with("received ") {
if trace.got_first_response_byte_value.load(Ordering::Relaxed) == 0 {
trace.got_first_response_byte();
}
} else if message.starts_with("flushed ") {
trace.written();
}
}
_ => {}
}
}
}
pub async fn request(
api: String,
http_request: HTTPRequest,
timeout: RequestTimeout,
) -> Result {
// 暂时使用单个实例,后续调整
// 如果多个请求并发,有可能数据不精确,暂时忽略
let trace = get_http_trace();
trace.reset();
let body = if http_request.content_type.starts_with("multipart/form-data") {
// 数据为base64
let buf = general_purpose::STANDARD.decode(http_request.body)?;
Body::from(buf)
} else {
Body::from(http_request.body)
};
let mut req = Request::new(body);
match http_request.method.to_uppercase().as_str() {
"POST" => *req.method_mut() = Method::POST,
"PUT" => *req.method_mut() = Method::PUT,
"DELETE" => *req.method_mut() = Method::DELETE,
"HEAD" => *req.method_mut() = Method::HEAD,
"OPTIONS" => *req.method_mut() = Method::OPTIONS,
"CONNECT" => *req.method_mut() = Method::CONNECT,
"PATCH" => *req.method_mut() = Method::PATCH,
"TRACE" => *req.method_mut() = Method::TRACE,
_ => *req.method_mut() = Method::GET,
};
// 设置query
let mut current_url = Url::parse(http_request.uri.as_str())?;
for q in http_request.query {
if !q.enabled {
continue;
}
current_url.query_pairs_mut().append_pair(&q.key, &q.value);
}
let request_uri = current_url.as_str().parse::()?;
*req.uri_mut() = request_uri;
// 设置header
let mut set_content_type = false;
let content_type = "content-type";
let header = req.headers_mut();
for h in http_request.headers {
if !h.enabled {
continue;
}
if h.key.to_lowercase() == content_type {
set_content_type = true;
}
header.insert(
h.key.parse::()?,
HeaderValue::from_str(h.value.as_str())?,
);
}
header.insert("Accept-Encoding", HeaderValue::from_str("gzip, br")?);
// 如果未设置content type
// 设置content type
if !set_content_type && !http_request.content_type.is_empty() {
header.insert(
content_type.parse::()?,
HeaderValue::from_str(http_request.content_type.as_str())?,
);
}
{
// cookie store未实现send,避免与下面的await冲突
// 设置Cookie
let cookie_store = cookies::get_cookie_store();
let cookie_header = cookie_store
.get_request_values(¤t_url)
.map(|(name, value)| format!("{}={}", name, value))
.collect::>()
.join("; ");
if !cookie_header.is_empty() {
header.insert(
"Cookie".parse::()?,
HeaderValue::from_str(cookie_header.as_str())?,
);
}
}
let connect_timeout = Duration::from_secs(timeout.connect);
let write_timeout = Duration::from_secs(timeout.write);
let read_timeout = Duration::from_secs(timeout.read);
// http 与 https使用不同的connector
let resp = if current_url.scheme() == "https" {
trace.tls();
let h = HttpsConnectorBuilder::new()
.with_native_roots()
.https_only()
.enable_http1()
.build();
let mut connector = TimeoutConnector::new(h);
connector.set_connect_timeout(Some(connect_timeout));
connector.set_read_timeout(Some(read_timeout));
connector.set_write_timeout(Some(write_timeout));
Client::builder()
.build::<_, hyper::Body>(connector)
.request(req)
.await?
} else {
let h = HttpConnector::new();
let mut connector = TimeoutConnector::new(h);
connector.set_connect_timeout(Some(connect_timeout));
connector.set_read_timeout(Some(read_timeout));
connector.set_write_timeout(Some(write_timeout));
Client::builder()
.build::<_, hyper::Body>(connector)
.request(req)
.await?
};
let status = resp.status().as_u16();
let mut headers = HashMap::new();
// let mut cookie_updated = false;
let mut set_cookies = Vec::new();
// 对响应的header处理
// 对于set-cookie记录至cookie store
let mut is_gzip = false;
let mut is_br = false;
let content_encoding_key = "content-encoding";
for (name, value) in resp.headers() {
let mut key = name.to_string();
key = key.to_lowercase();
let value = value.to_str()?.to_string();
if key == "set-cookie" {
set_cookies.push(value.clone());
}
if key == content_encoding_key {
if value == "gzip" {
is_gzip = true;
}
if value == "br" {
is_br = true;
}
}
// 响应的Header value处理
let values: Option<&Vec> = headers.get(&key);
match values {
Some(values) => {
let mut values = values.to_vec();
values.push(value);
headers.insert(key, values);
}
None => {
headers.insert(key, vec![value]);
}
}
}
// 如果有更新cookie,则写入
if !set_cookies.is_empty() {
cookies::save_cookie_store(set_cookies, ¤t_url)?;
}
let mut remote_addr = "".to_string();
if let Some(info) = resp.extensions().get::() {
remote_addr = info.remote_addr().to_string();
}
let mut buf = hyper::body::to_bytes(resp).await?;
// 主动触发done,不计算解压数据耗时
trace.done();
let body_size = buf.len();
// 解压gzip
if is_gzip {
let mut decoder = Decoder::new(&buf[..])?;
let mut decode_data = Vec::new();
let _ = decoder.read_to_end(&mut decode_data)?;
buf = Bytes::copy_from_slice(&decode_data);
}
// 解压br
if is_br {
let mut decode_data = Vec::new();
let mut r = buf.reader();
brotli_decompressor::BrotliDecompress(&mut r, &mut decode_data)?;
buf = Bytes::copy_from_slice(&decode_data);
}
let mut stats: HTTPStats = trace.into();
stats.remote_addr = remote_addr;
let resp = HTTPResponse {
api,
body_size: body_size as u32,
latency: stats.total,
status,
headers,
body: general_purpose::STANDARD.encode(buf),
stats,
};
Ok(resp)
}
================================================
FILE: src-tauri/src/main.rs
================================================
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
use tracing_subscriber::prelude::*;
mod commands;
mod cookies;
mod entities;
mod error;
mod http_request;
mod schemas;
mod util;
fn main() {
tracing_subscriber::registry()
.with(http_request::HTTPTraceLayer)
.init();
// // a builder for `FmtSubscriber`.
// let subscriber = FmtSubscriber::builder()
// // all spans/events with a level higher than TRACE (e.g, debug, info, warn, etc.)
// // will be written to stdout.
// .with_max_level(Level::TRACE)
// // completes the builder.
// .finish();
// tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
let context = tauri::generate_context!();
let menu = if cfg!(windows) {
tauri::Menu::new()
} else {
tauri::Menu::os_default(&context.package_info().name)
};
tauri::Builder::default()
.setup(|app| {
let dir = app.path_resolver().app_data_dir().unwrap();
util::set_app_dir(dir.to_str().unwrap().to_string());
Ok(())
})
.menu(menu)
.invoke_handler(tauri::generate_handler![
commands::close_splashscreen,
commands::init_tables,
commands::export_tables,
commands::import_tables,
commands::add_api_setting,
commands::update_api_setting,
commands::list_api_setting,
commands::delete_api_settings,
commands::add_api_folder,
commands::update_api_folder,
commands::list_api_folder,
commands::delete_api_folder,
commands::add_api_collection,
commands::update_api_collection,
commands::list_api_collection,
commands::delete_api_collection,
commands::do_http_request,
commands::list_cookie,
commands::delete_cookie,
commands::add_cookie,
commands::clear_cookie,
commands::add_variable,
commands::update_variable,
commands::delete_variable,
commands::list_variable,
commands::get_latest_version,
commands::add_version,
])
.run(context)
.expect("error while running tauri application");
}
================================================
FILE: src-tauri/src/schemas/api_collection.rs
================================================
use crate::{
entities::{api_collections, prelude::*},
error::CyberAPIError,
};
use chrono::Utc;
use sea_orm::{ActiveModelTrait, DbErr, EntityTrait, Set};
use serde::{Deserialize, Serialize};
use super::database::{get_database, ExportData};
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct APICollection {
// id
pub id: String,
// 名称
pub name: Option,
// 描述
pub description: Option,
// 创建时间
pub created_at: Option,
// 更新时间
pub updated_at: Option,
}
impl From for APICollection {
fn from(model: api_collections::Model) -> Self {
APICollection {
id: model.id,
name: model.name,
description: model.description,
created_at: model.created_at,
updated_at: model.updated_at,
}
}
}
impl APICollection {
fn into_active_model(self) -> api_collections::ActiveModel {
let created_at = self.created_at.or_else(|| Some(Utc::now().to_rfc3339()));
let updated_at = self.updated_at.or_else(|| Some(Utc::now().to_rfc3339()));
api_collections::ActiveModel {
id: Set(self.id),
name: Set(self.name),
description: Set(self.description),
created_at: Set(created_at),
updated_at: Set(updated_at),
}
}
}
pub fn get_api_collections_create_sql() -> String {
"CREATE TABLE IF NOT EXISTS api_collections (
id TEXT PRIMARY KEY NOT NULL check (id != ''),
name TEXT DEFAULT '',
description TEXT DEFAULT '',
created_at TEXT DEFAULT '',
updated_at TEXT DEFAULT ''
)"
.to_string()
}
pub async fn add_api_collection(collection: APICollection) -> Result {
let model: api_collections::ActiveModel = collection.into_active_model();
let db = get_database().await;
let result = model.insert(&db).await?;
Ok(result.into())
}
pub async fn update_api_collection(collection: APICollection) -> Result {
let model: api_collections::ActiveModel = collection.into_active_model();
let db = get_database().await;
let result = model.update(&db).await?;
Ok(result.into())
}
pub async fn list_api_collection() -> Result, DbErr> {
let db = get_database().await;
let result = ApiCollections::find().all(&db).await?;
Ok(result.into_iter().map(APICollection::from).collect())
}
pub async fn delete_api_collection(id: String) -> Result {
let db = get_database().await;
let result = ApiCollections::delete_by_id(id).exec(&db).await?;
Ok(result.rows_affected)
}
pub fn get_table_name_api_collection() -> String {
"api_collections".to_string()
}
pub async fn delete_all_api_collection() -> Result<(), DbErr> {
let db = get_database().await;
ApiCollections::delete_many().exec(&db).await?;
Ok(())
}
pub async fn export_api_collection() -> Result {
let db = get_database().await;
let data = ApiCollections::find().into_json().all(&db).await?;
Ok(ExportData {
name: get_table_name_api_collection(),
data,
})
}
pub async fn import_api_collection(data: Vec) -> Result<(), CyberAPIError> {
let db = get_database().await;
let mut records = Vec::new();
for ele in data {
let model = api_collections::ActiveModel::from_json(ele)?;
records.push(model);
}
ApiCollections::insert_many(records).exec(&db).await?;
Ok(())
}
================================================
FILE: src-tauri/src/schemas/api_folder.rs
================================================
use crate::{
entities::{api_folders, prelude::*},
error::CyberAPIError,
};
use chrono::Utc;
use sea_orm::{ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, QueryFilter, Set};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use super::database::{get_database, ExportData};
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct APIFolder {
// 目录ID
pub id: String,
// collection ID
pub collection: String,
// 子目录ID或API ID,以,分割
pub children: Option,
// 目录名称
pub name: Option,
// 创建时间
pub created_at: Option,
// 更新时间
pub updated_at: Option,
}
impl From for APIFolder {
fn from(model: api_folders::Model) -> Self {
APIFolder {
id: model.id,
collection: model.collection,
children: model.children,
name: model.name,
created_at: model.created_at,
updated_at: model.updated_at,
}
}
}
impl APIFolder {
fn into_active_model(self) -> api_folders::ActiveModel {
let created_at = self.created_at.or_else(|| Some(Utc::now().to_rfc3339()));
let updated_at = self.updated_at.or_else(|| Some(Utc::now().to_rfc3339()));
api_folders::ActiveModel {
id: Set(self.id),
collection: Set(self.collection),
children: Set(self.children),
name: Set(self.name),
created_at: Set(created_at),
updated_at: Set(updated_at),
}
}
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct APIFolderChildren {
pub folders: Vec,
pub settings: Vec,
}
pub fn get_api_folders_create_sql() -> String {
"CREATE TABLE IF NOT EXISTS api_folders (
id TEXT PRIMARY KEY NOT NULL check (id != ''),
collection TEXT NOT NULL check (collection != ''),
children TEXT DEFAULT '',
name TEXT DEFAULT '',
created_at TEXT DEFAULT '',
updated_at TEXT DEFAULT ''
)"
.to_string()
}
pub async fn add_api_folder(folder: APIFolder) -> Result {
let model = folder.into_active_model();
let db = get_database().await;
let result = model.insert(&db).await?;
Ok(result.into())
}
pub async fn update_api_folder(folder: APIFolder) -> Result {
let model = folder.into_active_model();
let db = get_database().await;
let result = model.update(&db).await?;
Ok(result.into())
}
pub async fn list_api_folder(collection: String) -> Result, DbErr> {
let db = get_database().await;
let result = ApiFolders::find()
.filter(api_folders::Column::Collection.eq(collection))
.all(&db)
.await?;
Ok(result.into_iter().map(APIFolder::from).collect())
}
pub async fn delete_api_folder_by_collection(collection: String) -> Result {
let db = get_database().await;
let result = ApiFolders::delete_many()
.filter(api_folders::Column::Collection.eq(collection))
.exec(&db)
.await?;
Ok(result.rows_affected)
}
pub async fn delete_api_folders(ids: Vec) -> Result {
let db = get_database().await;
let result = ApiFolders::delete_many()
.filter(api_folders::Column::Id.is_in(ids))
.exec(&db)
.await?;
Ok(result.rows_affected)
}
// 获取该目录的所有子元素(包括子元素以及子目录、子目录的子元素)
pub async fn list_api_folder_all_children(id: String) -> Result {
// 使用偷懒的方式,直接查询所有api folder再过滤
let mut folder_children = HashMap::new();
let mut folders = Vec::new();
let mut settings = Vec::new();
let mut children = "".to_string();
let db = get_database().await;
let current_folder = ApiFolders::find()
.filter(api_folders::Column::Id.eq(id.clone()))
.one(&db)
.await?;
if let Some(folder) = current_folder {
// 记录所有folder与它的子目录
for ele in list_api_folder(folder.collection).await? {
if ele.id == id {
if let Some(data) = ele.children.clone() {
children = data
}
}
folder_children.insert(ele.id, ele.children.clone());
}
}
while !children.is_empty() {
let arr = children.split(',');
let mut current_children = Vec::new();
for ele in arr {
let id = ele.trim();
// 是folder
match folder_children.get(id) {
// 目录
Some(str) => {
folders.push(id.to_string());
if let Some(value) = str {
// 记录子元素
if !value.is_empty() {
current_children.push(value.as_str());
}
}
}
None => {
// api settings
settings.push(id.to_string())
}
}
}
// 记录新的children
children = current_children.join(",")
}
Ok(APIFolderChildren { folders, settings })
}
pub fn get_table_name_api_folder() -> String {
"api_folders".to_string()
}
pub async fn delete_all_api_folder() -> Result<(), CyberAPIError> {
let db = get_database().await;
ApiFolders::delete_many().exec(&db).await?;
Ok(())
}
pub async fn export_api_folder() -> Result {
let db = get_database().await;
let data = ApiFolders::find().into_json().all(&db).await?;
Ok(ExportData {
name: get_table_name_api_folder(),
data,
})
}
pub async fn import_api_folder(data: Vec) -> Result<(), CyberAPIError> {
let db = get_database().await;
let mut records = Vec::new();
for ele in data {
let model = api_folders::ActiveModel::from_json(ele)?;
records.push(model);
}
ApiFolders::insert_many(records).exec(&db).await?;
Ok(())
}
================================================
FILE: src-tauri/src/schemas/api_setting.rs
================================================
use crate::{
entities::{api_settings, prelude::*},
error::CyberAPIError,
};
use chrono::Utc;
use sea_orm::{ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, QueryFilter, Set};
use serde::{Deserialize, Serialize};
use super::database::{get_database, ExportData};
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct APISetting {
// id
pub id: String,
// collection ID
pub collection: String,
// 配置名称
pub name: Option,
// 类型(http, graphQL)
pub category: Option,
// 配置信息
pub setting: Option,
// 创建时间
pub created_at: Option,
// 更新时间
pub updated_at: Option,
}
impl From for APISetting {
fn from(model: api_settings::Model) -> Self {
APISetting {
id: model.id,
collection: model.collection,
name: model.name,
category: model.category,
setting: model.setting,
created_at: model.created_at,
updated_at: model.updated_at,
}
}
}
impl APISetting {
fn into_active_model(self) -> api_settings::ActiveModel {
let created_at = self.created_at.or_else(|| Some(Utc::now().to_rfc3339()));
let updated_at = self.updated_at.or_else(|| Some(Utc::now().to_rfc3339()));
api_settings::ActiveModel {
id: Set(self.id),
collection: Set(self.collection),
name: Set(self.name),
category: Set(self.category),
setting: Set(self.setting),
created_at: Set(created_at),
updated_at: Set(updated_at),
}
}
}
pub fn get_api_settings_create_sql() -> String {
"CREATE TABLE IF NOT EXISTS api_settings (
id TEXT PRIMARY KEY NOT NULL check (id != ''),
collection TEXT NOT NULL check (collection != ''),
name TEXT DEFAULT '',
category TEXT DEFAULT '',
setting TEXT DEFAULT '',
created_at TEXT DEFAULT '',
updated_at TEXT DEFAULT ''
)"
.to_string()
}
pub async fn add_api_setting(setting: APISetting) -> Result {
let model = setting.into_active_model();
let db = get_database().await;
let result = model.insert(&db).await?;
Ok(result.into())
}
pub async fn update_api_setting(setting: APISetting) -> Result {
let model = setting.into_active_model();
let db = get_database().await;
let result = model.update(&db).await?;
Ok(result.into())
}
pub async fn list_api_setting(collection: String) -> Result, DbErr> {
let db = get_database().await;
let result = ApiSettings::find()
.filter(api_settings::Column::Collection.eq(collection))
.all(&db)
.await?;
Ok(result.into_iter().map(APISetting::from).collect())
}
pub async fn delete_api_setting_by_collection(collection: String) -> Result {
let db = get_database().await;
let result = ApiSettings::delete_many()
.filter(api_settings::Column::Collection.eq(collection))
.exec(&db)
.await?;
Ok(result.rows_affected)
}
pub async fn delete_api_settings(ids: Vec) -> Result {
let db = get_database().await;
let result = ApiSettings::delete_many()
.filter(api_settings::Column::Id.is_in(ids))
.exec(&db)
.await?;
Ok(result.rows_affected)
}
pub fn get_table_name_api_setting() -> String {
"api_settings".to_string()
}
pub async fn delete_all_api_setting() -> Result<(), CyberAPIError> {
let db = get_database().await;
ApiSettings::delete_many().exec(&db).await?;
Ok(())
}
pub async fn export_api_setting() -> Result {
let db = get_database().await;
let data = ApiSettings::find().into_json().all(&db).await?;
Ok(ExportData {
name: get_table_name_api_setting(),
data,
})
}
pub async fn import_api_setting(data: Vec) -> Result<(), CyberAPIError> {
let db = get_database().await;
let mut records = Vec::new();
for ele in data {
let model = api_settings::ActiveModel::from_json(ele)?;
records.push(model);
}
ApiSettings::insert_many(records).exec(&db).await?;
Ok(())
}
================================================
FILE: src-tauri/src/schemas/database.rs
================================================
use crate::error::CyberAPIError;
use chrono::Local;
use sea_orm::{ConnectOptions, ConnectionTrait, Database, DatabaseConnection, DbErr, Statement};
use std::fs::OpenOptions;
use std::io::{Read, Write};
use std::time::Duration;
use std::vec;
use std::{fs, fs::File, path::Path};
use tauri::api::path::download_dir;
use tokio::sync::OnceCell;
use zip::write::FileOptions;
use crate::util;
use super::api_collection::{
delete_all_api_collection, export_api_collection, get_api_collections_create_sql,
get_table_name_api_collection, import_api_collection,
};
use super::api_folder::{
delete_all_api_folder, export_api_folder, get_api_folders_create_sql,
get_table_name_api_folder, import_api_folder,
};
use super::api_setting::{
delete_all_api_setting, export_api_setting, get_api_settings_create_sql,
get_table_name_api_setting, import_api_setting,
};
use super::variable::{
delete_all_variable, export_variable, get_table_name_variable, get_variables_create_sql,
import_variable,
};
use super::version::get_versions_table_create_sql;
static DB: OnceCell = OnceCell::const_new();
pub struct ExportData {
pub name: String,
pub data: Vec,
}
async fn get_conn() -> DatabaseConnection {
let dir = Path::new(util::get_app_dir());
let file = dir.join("my_db.db");
fs::create_dir_all(dir).unwrap();
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(file.clone())
.unwrap();
let conn_uri = format!("sqlite://{}", file.into_os_string().into_string().unwrap());
let mut opt = ConnectOptions::new(conn_uri);
opt.max_connections(10)
.min_connections(2)
.connect_timeout(Duration::from_secs(5))
.idle_timeout(Duration::from_secs(60));
let result = Database::connect(opt).await;
result.unwrap()
}
pub async fn get_database() -> DatabaseConnection {
let db = DB.get_or_init(get_conn).await;
db.to_owned()
}
pub async fn init_tables() -> Result<(), DbErr> {
let db = get_database().await;
let init_sql_list = vec![
get_versions_table_create_sql(),
get_api_collections_create_sql(),
get_api_folders_create_sql(),
get_api_settings_create_sql(),
get_variables_create_sql(),
];
for sql in init_sql_list {
db.execute(Statement::from_string(db.get_database_backend(), sql))
.await?;
}
Ok(())
}
pub async fn export_tables() -> Result {
let download = download_dir().unwrap();
let local = Local::now();
let filename = format!("cyberapi-backup-{}.zip", local.format("%Y-%m-%d"));
let file = File::create(&download.join(filename.clone()))?;
let mut w = zip::ZipWriter::new(file);
let table_data_list = vec![
export_api_collection().await?,
export_api_folder().await?,
export_api_setting().await?,
export_variable().await?,
];
let options = FileOptions::default()
.compression_method(zip::CompressionMethod::Deflated)
.unix_permissions(0o755);
for table_data in table_data_list {
let mut json = vec![];
for ele in table_data.data {
let str = serde_json::to_string(&ele)?;
json.push(str);
}
let file_name = table_data.name + ".json";
w.start_file(file_name, options)?;
let file_data = format!("[{}]", json.join(","));
w.write_all(file_data.as_bytes())?;
}
w.finish()?;
Ok(filename)
}
pub async fn import_tables(filename: String) -> Result<(), CyberAPIError> {
let mut r = zip::ZipArchive::new(File::open(filename)?)?;
delete_all_api_collection().await?;
delete_all_api_folder().await?;
delete_all_api_setting().await?;
delete_all_variable().await?;
let names = vec![
get_table_name_api_collection(),
get_table_name_api_folder(),
get_table_name_api_setting(),
get_table_name_variable(),
];
for i in 0..names.len() {
let name = names.get(i).unwrap();
let mut buf = Vec::new();
{
let mut file = r.by_name((name.to_owned() + ".json").as_str())?;
file.read_to_end(&mut buf)?;
}
let data: Vec = serde_json::from_slice(&buf)?;
match i {
0 => import_api_collection(data).await?,
1 => import_api_folder(data).await?,
2 => import_api_setting(data).await?,
3 => import_variable(data).await?,
_ => (),
}
}
Ok(())
}
================================================
FILE: src-tauri/src/schemas/mod.rs
================================================
mod api_collection;
mod api_folder;
mod api_setting;
mod database;
mod variable;
mod version;
pub use database::{export_tables, import_tables, init_tables};
pub use api_collection::{
add_api_collection, delete_api_collection, list_api_collection, update_api_collection,
APICollection,
};
pub use api_folder::{
add_api_folder, delete_api_folder_by_collection, delete_api_folders, list_api_folder,
list_api_folder_all_children, update_api_folder, APIFolder, APIFolderChildren,
};
pub use api_setting::{
add_api_setting, delete_api_setting_by_collection, delete_api_settings, list_api_setting,
update_api_setting, APISetting,
};
pub use variable::{add_variable, delete_variable, list_variable, update_variable, Variable};
pub use version::{add_version, get_latest_version, Version};
================================================
FILE: src-tauri/src/schemas/variable.rs
================================================
use crate::{
entities::{prelude::*, variables},
error::CyberAPIError,
};
use chrono::Utc;
use sea_orm::{ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, QueryFilter, Set};
use serde::{Deserialize, Serialize};
use super::database::{get_database, ExportData};
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Variable {
// id
pub id: String,
// 分类
pub category: String,
// collection ID
pub collection: String,
// 变量名称
pub name: Option,
// 变量值
pub value: Option,
// 是否启用(0:禁用 1:启用)
pub enabled: Option,
// 创建时间
pub created_at: Option,
// 更新时间
pub updated_at: Option,
}
impl From for Variable {
fn from(model: variables::Model) -> Self {
Variable {
id: model.id,
category: model.category,
collection: model.collection,
name: model.name,
value: model.value,
enabled: model.enabled,
created_at: model.created_at,
updated_at: model.updated_at,
}
}
}
impl Variable {
fn into_active_model(self) -> variables::ActiveModel {
let created_at = self.created_at.or_else(|| Some(Utc::now().to_rfc3339()));
let updated_at = self.updated_at.or_else(|| Some(Utc::now().to_rfc3339()));
variables::ActiveModel {
id: Set(self.id),
category: Set(self.category),
collection: Set(self.collection),
name: Set(self.name),
value: Set(self.value),
enabled: Set(self.enabled),
created_at: Set(created_at),
updated_at: Set(updated_at),
}
}
}
pub fn get_variables_create_sql() -> String {
"CREATE TABLE IF NOT EXISTS variables (
id TEXT PRIMARY KEY NOT NULL check (id != ''),
category TEXT NOT NULL check (category != ''),
collection TEXT NOT NULL check (collection != ''),
name TEXT DEFAULT '',
value TEXT DEFAULT '',
enabled TEXT DEFAULT '',
created_at TEXT DEFAULT '',
updated_at TEXT DEFAULT ''
)"
.to_string()
}
pub async fn add_variable(value: Variable) -> Result {
let model = value.into_active_model();
let db = get_database().await;
let result = model.insert(&db).await?;
Ok(result.into())
}
pub async fn update_variable(value: Variable) -> Result {
let model = value.into_active_model();
let db = get_database().await;
let result = model.update(&db).await?;
Ok(result.into())
}
pub async fn list_variable(collection: String, category: String) -> Result, DbErr> {
let db = get_database().await;
let result = Variables::find()
.filter(variables::Column::Collection.eq(collection))
.filter(variables::Column::Category.eq(category))
.all(&db)
.await?;
Ok(result.into_iter().map(Variable::from).collect())
}
pub async fn delete_variable(ids: Vec) -> Result {
let db = get_database().await;
let result = Variables::delete_many()
.filter(variables::Column::Id.is_in(ids))
.exec(&db)
.await?;
Ok(result.rows_affected)
}
pub fn get_table_name_variable() -> String {
"variables".to_string()
}
pub async fn delete_all_variable() -> Result<(), CyberAPIError> {
let db = get_database().await;
Variables::delete_many().exec(&db).await?;
Ok(())
}
pub async fn export_variable() -> Result {
let db = get_database().await;
let data = Variables::find().into_json().all(&db).await?;
Ok(ExportData {
name: get_table_name_variable(),
data,
})
}
pub async fn import_variable(data: Vec) -> Result<(), CyberAPIError> {
let db = get_database().await;
let mut records = Vec::new();
for ele in data {
let model = variables::ActiveModel::from_json(ele)?;
records.push(model);
}
Variables::insert_many(records).exec(&db).await?;
Ok(())
}
================================================
FILE: src-tauri/src/schemas/version.rs
================================================
use crate::entities::{prelude::*, versions};
use chrono::Utc;
use sea_orm::{ActiveModelTrait, DbErr, EntityTrait, QueryOrder, Set};
use serde::{Deserialize, Serialize};
use super::database::get_database;
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Version {
// id
pub id: String,
// 版本号
pub version: String,
// 创建时间
pub created_at: Option