init
This commit is contained in:
1 parent
7e430424fe
commit
2a6ec64e67
65 files changed
+10424
No files matched your search
@@ -0,0 +1,201 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ github.ref_type }}" = "tag" ]; then
|
||||
echo "version=${{ github.ref_name }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "version=dev-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Install Wails
|
||||
run: go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
wails build `
|
||||
-platform windows/amd64 `
|
||||
-ldflags "-X webhelper/global.Version=${{ steps.version.outputs.version }}" `
|
||||
-o webhelper-windows-amd64.exe
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: webhelper-windows-amd64
|
||||
path: build/bin/webhelper-windows-amd64.exe
|
||||
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: |
|
||||
if [ "${{ github.ref_type }}" = "tag" ]; then
|
||||
echo "version=${{ github.ref_name }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "version=dev-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Install Linux dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libgtk-3-dev \
|
||||
libwebkit2gtk-4.0-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev \
|
||||
patchelf
|
||||
|
||||
- name: Install Wails
|
||||
run: go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||
|
||||
- name: Build amd64
|
||||
env:
|
||||
CGO_ENABLED: 1
|
||||
GOOS: linux
|
||||
GOARCH: amd64
|
||||
run: |
|
||||
wails build \
|
||||
-platform linux/amd64 \
|
||||
-ldflags "-X webhelper/global.Version=${{ steps.version.outputs.version }}" \
|
||||
-o webhelper-linux-amd64
|
||||
|
||||
- name: Build arm64
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: linux
|
||||
GOARCH: arm64
|
||||
run: |
|
||||
wails build \
|
||||
-platform linux/arm64 \
|
||||
-ldflags "-X webhelper/global.Version=${{ steps.version.outputs.version }}" \
|
||||
-o webhelper-linux-arm64
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: |
|
||||
build/bin/webhelper-linux-amd64
|
||||
build/bin/webhelper-linux-arm64
|
||||
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
run: |
|
||||
if [ "${{ github.ref_type }}" = "tag" ]; then
|
||||
echo "version=${{ github.ref_name }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "version=dev-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Install Wails
|
||||
run: go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||
|
||||
- name: Build amd64
|
||||
env:
|
||||
CGO_ENABLED: 1
|
||||
GOOS: darwin
|
||||
GOARCH: amd64
|
||||
run: |
|
||||
wails build \
|
||||
-platform darwin/amd64 \
|
||||
-ldflags "-X webhelper/global.Version=${{ steps.version.outputs.version }}" \
|
||||
-o webhelper-darwin-amd64
|
||||
|
||||
- name: Build arm64
|
||||
env:
|
||||
CGO_ENABLED: 1
|
||||
GOOS: darwin
|
||||
GOARCH: arm64
|
||||
run: |
|
||||
wails build \
|
||||
-platform darwin/arm64 \
|
||||
-ldflags "-X webhelper/global.Version=${{ steps.version.outputs.version }}" \
|
||||
-o webhelper-darwin-arm64
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: |
|
||||
build/bin/webhelper-darwin-amd64
|
||||
build/bin/webhelper-darwin-arm64
|
||||
|
||||
release:
|
||||
needs: [build-windows, build-linux, build-macos]
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
generate_release_notes: true
|
||||
files: artifacts/*
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Code coverage profiles and other test artifacts
|
||||
*.out
|
||||
coverage.*
|
||||
*.coverprofile
|
||||
profile.cov
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
.workbuddy/
|
||||
.codebuddy/
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# WebHelper
|
||||
|
||||
基于 Wails v2 + Vue 3 + Element Plus 的网页调试助手桌面应用。
|
||||
|
||||
## 功能
|
||||
|
||||
- **网页调试** — 发送 HTTP 请求,支持所有方法、请求头、Cookie、代理、文件上传、请求日志
|
||||
- **JS 调试** — 内置 JavaScript 执行引擎,支持 console.log 捕获和返回值
|
||||
- **JSON 解析** — 树形/表格/原始三种视图,支持从网页调试一键发送
|
||||
- **Socket 调试** — WebSocket/TCP 连接、收发消息、实时日志
|
||||
- **多语言** — 内置简体中文、English、日本語、한국어,运行时切换
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 层级 | 技术 |
|
||||
|------|------|
|
||||
| 后端 | Go + Wails v2 + goja (JS引擎) + gorilla/websocket |
|
||||
| 前端 | Vue 3 + Element Plus + Vite |
|
||||
| 国际化 | 自定义 i18n,Go 后端加载,`go:embed` 打包 |
|
||||
|
||||
## 环境要求
|
||||
|
||||
- [Go](https://go.dev/dl/) >= 1.23
|
||||
- [Node.js](https://nodejs.org/) >= 18
|
||||
- [Wails CLI](https://wails.io/docs/gettingstarted/installation) v2
|
||||
|
||||
```bash
|
||||
go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||
```
|
||||
|
||||
## 运行
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
cd frontend && npm install && cd ..
|
||||
|
||||
# 开发模式
|
||||
wails dev -tags debug
|
||||
|
||||
# 构建生产版本
|
||||
wails build
|
||||
```
|
||||
|
||||
## 构建指定版本
|
||||
|
||||
```bash
|
||||
wails build -ldflags "-X webhelper/global.Version=v1.0.0"
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
├── api/ # API 层 (HTTP/JS/Socket/Log)
|
||||
├── global/ # 全局配置、日志、语言包、版本号
|
||||
├── lang/ # 语言包 (go:embed 打包)
|
||||
│ ├── default/ # 简体中文
|
||||
│ ├── en/ # English
|
||||
│ ├── ja/ # 日本語
|
||||
│ └── ko/ # 한국어
|
||||
├── logger/ # 日志系统
|
||||
├── service/ # Wails 绑定方法
|
||||
├── frontend/ # Vue 3 前端
|
||||
│ └── src/components/ # 5 个功能页面
|
||||
├── .github/workflows/ # CI/CD 自动构建
|
||||
├── main.go # 程序入口
|
||||
└── wails.json
|
||||
```
|
||||
|
||||
## 添加语言
|
||||
|
||||
在 `lang/` 下新建目录,包含 `info.json` 和 `textmap.json`:
|
||||
|
||||
```json
|
||||
// lang/ja/info.json
|
||||
{
|
||||
"language_name": "日本語",
|
||||
"language_code": "ja-JP",
|
||||
"textmap_path": "textmap.json",
|
||||
"translation_progress": "100%",
|
||||
"translator": "",
|
||||
"last_updated": "",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
重启后自动出现在语言切换列表中。
|
||||
|
||||
## CI/CD
|
||||
|
||||
推送 `v*` 标签自动构建多平台版本并发布 Release:
|
||||
|
||||
```bash
|
||||
git tag v1.0.0
|
||||
git push origin v1.0.0
|
||||
```
|
||||
|
||||
构建目标:Windows (amd64)、Linux (amd64/arm64)、macOS (amd64/arm64)
|
||||
|
||||
## License
|
||||
|
||||
GPL-3.0
|
||||
+495
@@ -0,0 +1,495 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HttpRequest 请求结构体
|
||||
type HttpRequest struct {
|
||||
Method string `json:"method"` // HTTP 方法
|
||||
URL string `json:"url"` // 请求地址
|
||||
Headers map[string]string `json:"headers"` // 请求头
|
||||
Cookies map[string]string `json:"cookies"` // Cookie
|
||||
Proxy string `json:"proxy"` // 代理地址
|
||||
BodyType string `json:"bodyType"` // 数据类型: text, file, form, json
|
||||
Body string `json:"body"` // 文本数据
|
||||
FilePath string `json:"filePath"` // 文件路径
|
||||
ContentType string `json:"contentType"` // Content-Type
|
||||
Timeout int `json:"timeout"` // 超时时间(秒)
|
||||
Insecure bool `json:"insecure"` // 忽略证书验证
|
||||
}
|
||||
|
||||
// HttpResponse 响应结构体
|
||||
type HttpResponse struct {
|
||||
StatusCode int `json:"statusCode"` // 状态码
|
||||
Status string `json:"status"` // 状态描述
|
||||
Headers map[string]string `json:"headers"` // 响应头
|
||||
Cookies []CookieInfo `json:"cookies"` // 响应 Cookie
|
||||
Body string `json:"body"` // 响应正文
|
||||
ContentType string `json:"contentType"` // Content-Type
|
||||
ContentLength int64 `json:"contentLength"` // 内容长度
|
||||
Time string `json:"time"` // 请求耗时
|
||||
Size int64 `json:"size"` // 响应大小
|
||||
Error string `json:"error"` // 错误信息
|
||||
}
|
||||
|
||||
// CookieInfo Cookie 信息
|
||||
type CookieInfo struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Path string `json:"path"`
|
||||
Domain string `json:"domain"`
|
||||
Expires string `json:"expires"`
|
||||
MaxAge int `json:"maxAge"`
|
||||
Secure bool `json:"secure"`
|
||||
HttpOnly bool `json:"httpOnly"`
|
||||
SameSite string `json:"sameSite"`
|
||||
}
|
||||
|
||||
// FileData 文件数据
|
||||
type FileData struct {
|
||||
FieldName string `json:"fieldName"` // 字段名
|
||||
FileName string `json:"fileName"` // 文件名
|
||||
ContentType string `json:"contentType"` // 文件类型
|
||||
Path string `json:"path"` // 文件路径
|
||||
}
|
||||
|
||||
// SendRequest 发送 HTTP 请求
|
||||
func SendRequest(req *HttpRequest) *HttpResponse {
|
||||
resp := &HttpResponse{
|
||||
Headers: make(map[string]string),
|
||||
}
|
||||
|
||||
if req.URL == "" {
|
||||
resp.Error = "请求地址不能为空"
|
||||
return resp
|
||||
}
|
||||
|
||||
// 补全协议
|
||||
if !strings.HasPrefix(req.URL, "http://") && !strings.HasPrefix(req.URL, "https://") {
|
||||
req.URL = "https://" + req.URL
|
||||
}
|
||||
|
||||
// 设置超时
|
||||
timeout := time.Duration(req.Timeout) * time.Second
|
||||
if timeout == 0 {
|
||||
timeout = 30 * time.Second
|
||||
}
|
||||
|
||||
// 创建 HTTP 客户端
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
// 配置代理
|
||||
if req.Proxy != "" {
|
||||
proxyURL, err := url.Parse(req.Proxy)
|
||||
if err != nil {
|
||||
resp.Error = fmt.Sprintf("代理地址格式错误: %v", err)
|
||||
return resp
|
||||
}
|
||||
client.Transport = &http.Transport{
|
||||
Proxy: http.ProxyURL(proxyURL),
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: req.Insecure,
|
||||
},
|
||||
}
|
||||
} else if req.Insecure {
|
||||
client.Transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 创建请求体
|
||||
var bodyReader io.Reader
|
||||
contentType := req.ContentType
|
||||
|
||||
if req.Method != http.MethodGet && req.Method != http.MethodHead {
|
||||
switch req.BodyType {
|
||||
case "file":
|
||||
// 文件上传
|
||||
if req.FilePath != "" {
|
||||
file, err := os.Open(req.FilePath)
|
||||
if err != nil {
|
||||
resp.Error = fmt.Sprintf("打开文件失败: %v", err)
|
||||
return resp
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 获取文件信息
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
resp.Error = fmt.Sprintf("获取文件信息失败: %v", err)
|
||||
return resp
|
||||
}
|
||||
|
||||
// 使用 multipart 上传
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("file", fileInfo.Name())
|
||||
if err != nil {
|
||||
resp.Error = fmt.Sprintf("创建表单文件失败: %v", err)
|
||||
return resp
|
||||
}
|
||||
if _, err := io.Copy(part, file); err != nil {
|
||||
resp.Error = fmt.Sprintf("复制文件内容失败: %v", err)
|
||||
return resp
|
||||
}
|
||||
writer.Close()
|
||||
bodyReader = body
|
||||
contentType = writer.FormDataContentType()
|
||||
}
|
||||
|
||||
case "form":
|
||||
// 表单数据
|
||||
if req.Body != "" {
|
||||
bodyReader = strings.NewReader(req.Body)
|
||||
if contentType == "" {
|
||||
contentType = "application/x-www-form-urlencoded"
|
||||
}
|
||||
}
|
||||
|
||||
case "json":
|
||||
// JSON 数据
|
||||
if req.Body != "" {
|
||||
bodyReader = strings.NewReader(req.Body)
|
||||
if contentType == "" {
|
||||
contentType = "application/json"
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
// 文本数据
|
||||
if req.Body != "" {
|
||||
bodyReader = strings.NewReader(req.Body)
|
||||
if contentType == "" {
|
||||
contentType = "text/plain"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建请求
|
||||
httpReq, err := http.NewRequest(req.Method, req.URL, bodyReader)
|
||||
if err != nil {
|
||||
resp.Error = fmt.Sprintf("创建请求失败: %v", err)
|
||||
return resp
|
||||
}
|
||||
|
||||
// 设置 Content-Type
|
||||
if contentType != "" {
|
||||
httpReq.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
|
||||
// 设置自定义请求头(需要单独处理 host)
|
||||
for key, value := range req.Headers {
|
||||
if strings.ToLower(key) == "host" {
|
||||
httpReq.Host = value
|
||||
} else {
|
||||
httpReq.Header.Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置 Cookie
|
||||
if len(req.Cookies) > 0 {
|
||||
cookieJar, _ := cookiejar.New(nil)
|
||||
var cookieList []*http.Cookie
|
||||
for name, value := range req.Cookies {
|
||||
cookieList = append(cookieList, &http.Cookie{
|
||||
Name: name,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
parsedURL, _ := url.Parse(req.URL)
|
||||
cookieJar.SetCookies(parsedURL, cookieList)
|
||||
client.Jar = cookieJar
|
||||
}
|
||||
|
||||
// 记录开始时间
|
||||
start := time.Now()
|
||||
|
||||
// 发送请求
|
||||
httpResp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
resp.Error = fmt.Sprintf("请求失败: %v", err)
|
||||
return resp
|
||||
}
|
||||
defer httpResp.Body.Close()
|
||||
|
||||
// 记录耗时
|
||||
resp.Time = time.Since(start).String()
|
||||
|
||||
// 读取响应体
|
||||
body, err := io.ReadAll(httpResp.Body)
|
||||
if err != nil {
|
||||
resp.Error = fmt.Sprintf("读取响应失败: %v", err)
|
||||
return resp
|
||||
}
|
||||
|
||||
// 设置响应
|
||||
resp.StatusCode = httpResp.StatusCode
|
||||
resp.Status = httpResp.Status
|
||||
resp.Body = string(body)
|
||||
resp.ContentLength = httpResp.ContentLength
|
||||
resp.Size = int64(len(body))
|
||||
|
||||
// 获取 Content-Type
|
||||
resp.ContentType = httpResp.Header.Get("Content-Type")
|
||||
|
||||
// 获取响应头
|
||||
for key, values := range httpResp.Header {
|
||||
resp.Headers[key] = strings.Join(values, "; ")
|
||||
}
|
||||
|
||||
// 获取响应 Cookie
|
||||
resp.Cookies = convertCookies(httpResp.Cookies())
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// SendRequestWithFiles 带文件的请求
|
||||
func SendRequestWithFiles(req *HttpRequest, files []FileData) *HttpResponse {
|
||||
resp := &HttpResponse{
|
||||
Headers: make(map[string]string),
|
||||
}
|
||||
|
||||
if req.URL == "" {
|
||||
resp.Error = "请求地址不能为空"
|
||||
return resp
|
||||
}
|
||||
|
||||
// 补全协议
|
||||
if !strings.HasPrefix(req.URL, "http://") && !strings.HasPrefix(req.URL, "https://") {
|
||||
req.URL = "https://" + req.URL
|
||||
}
|
||||
|
||||
// 设置超时
|
||||
timeout := time.Duration(req.Timeout) * time.Second
|
||||
if timeout == 0 {
|
||||
timeout = 30 * time.Second
|
||||
}
|
||||
|
||||
// 创建 HTTP 客户端
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
// 配置代理
|
||||
if req.Proxy != "" {
|
||||
proxyURL, err := url.Parse(req.Proxy)
|
||||
if err != nil {
|
||||
resp.Error = fmt.Sprintf("代理地址格式错误: %v", err)
|
||||
return resp
|
||||
}
|
||||
client.Transport = &http.Transport{
|
||||
Proxy: http.ProxyURL(proxyURL),
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: req.Insecure,
|
||||
},
|
||||
}
|
||||
} else if req.Insecure {
|
||||
client.Transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 创建 multipart 请求
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
// 添加文本字段
|
||||
if req.Body != "" {
|
||||
_ = writer.WriteField("text", req.Body)
|
||||
}
|
||||
|
||||
// 添加文件
|
||||
for _, file := range files {
|
||||
f, err := os.Open(file.Path)
|
||||
if err != nil {
|
||||
resp.Error = fmt.Sprintf("打开文件 %s 失败: %v", file.Path, err)
|
||||
return resp
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
part, err := writer.CreateFormFile(file.FieldName, file.FileName)
|
||||
if err != nil {
|
||||
resp.Error = fmt.Sprintf("创建文件字段失败: %v", err)
|
||||
return resp
|
||||
}
|
||||
|
||||
if _, err := io.Copy(part, f); err != nil {
|
||||
resp.Error = fmt.Sprintf("复制文件内容失败: %v", err)
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
writer.Close()
|
||||
|
||||
// 创建请求
|
||||
httpReq, err := http.NewRequest(req.Method, req.URL, body)
|
||||
if err != nil {
|
||||
resp.Error = fmt.Sprintf("创建请求失败: %v", err)
|
||||
return resp
|
||||
}
|
||||
|
||||
// 设置 Content-Type
|
||||
httpReq.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
// 设置自定义请求头(需要单独处理 host)
|
||||
for key, value := range req.Headers {
|
||||
if strings.ToLower(key) == "host" {
|
||||
httpReq.Host = value
|
||||
} else {
|
||||
httpReq.Header.Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置 Cookie
|
||||
if len(req.Cookies) > 0 {
|
||||
cookieJar, _ := cookiejar.New(nil)
|
||||
var cookieList []*http.Cookie
|
||||
for name, value := range req.Cookies {
|
||||
cookieList = append(cookieList, &http.Cookie{
|
||||
Name: name,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
parsedURL, _ := url.Parse(req.URL)
|
||||
cookieJar.SetCookies(parsedURL, cookieList)
|
||||
client.Jar = cookieJar
|
||||
}
|
||||
|
||||
// 记录开始时间
|
||||
start := time.Now()
|
||||
|
||||
// 发送请求
|
||||
httpResp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
resp.Error = fmt.Sprintf("请求失败: %v", err)
|
||||
return resp
|
||||
}
|
||||
defer httpResp.Body.Close()
|
||||
|
||||
// 记录耗时
|
||||
resp.Time = time.Since(start).String()
|
||||
|
||||
// 读取响应体
|
||||
respBody, err := io.ReadAll(httpResp.Body)
|
||||
if err != nil {
|
||||
resp.Error = fmt.Sprintf("读取响应失败: %v", err)
|
||||
return resp
|
||||
}
|
||||
|
||||
// 设置响应
|
||||
resp.StatusCode = httpResp.StatusCode
|
||||
resp.Status = httpResp.Status
|
||||
resp.Body = string(respBody)
|
||||
resp.ContentLength = httpResp.ContentLength
|
||||
resp.Size = int64(len(respBody))
|
||||
|
||||
// 获取 Content-Type
|
||||
resp.ContentType = httpResp.Header.Get("Content-Type")
|
||||
|
||||
// 获取响应头
|
||||
for key, values := range httpResp.Header {
|
||||
resp.Headers[key] = strings.Join(values, "; ")
|
||||
}
|
||||
|
||||
// 获取响应 Cookie
|
||||
resp.Cookies = convertCookies(httpResp.Cookies())
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// ParseHeaders 解析请求头字符串
|
||||
func ParseHeaders(headerStr string) map[string]string {
|
||||
headers := make(map[string]string)
|
||||
if headerStr == "" {
|
||||
return headers
|
||||
}
|
||||
|
||||
lines := strings.Split(headerStr, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
headers[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
// ParseCookies 解析 Cookie 字符串
|
||||
func ParseCookies(cookieStr string) map[string]string {
|
||||
cookies := make(map[string]string)
|
||||
if cookieStr == "" {
|
||||
return cookies
|
||||
}
|
||||
|
||||
pairs := strings.Split(cookieStr, ";")
|
||||
for _, pair := range pairs {
|
||||
pair = strings.TrimSpace(pair)
|
||||
if pair == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(pair, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
name := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
cookies[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
return cookies
|
||||
}
|
||||
|
||||
// convertCookies 将 http.Cookie 转换为 CookieInfo
|
||||
func convertCookies(httpCookies []*http.Cookie) []CookieInfo {
|
||||
var cookies []CookieInfo
|
||||
for _, c := range httpCookies {
|
||||
sameSite := ""
|
||||
switch c.SameSite {
|
||||
case http.SameSiteStrictMode:
|
||||
sameSite = "Strict"
|
||||
case http.SameSiteLaxMode:
|
||||
sameSite = "Lax"
|
||||
case http.SameSiteNoneMode:
|
||||
sameSite = "None"
|
||||
}
|
||||
|
||||
cookies = append(cookies, CookieInfo{
|
||||
Name: c.Name,
|
||||
Value: c.Value,
|
||||
Path: c.Path,
|
||||
Domain: c.Domain,
|
||||
Expires: c.Expires.Format(time.RFC1123),
|
||||
MaxAge: c.MaxAge,
|
||||
Secure: c.Secure,
|
||||
HttpOnly: c.HttpOnly,
|
||||
SameSite: sameSite,
|
||||
})
|
||||
}
|
||||
return cookies
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
// JsExecResult JS 执行结果
|
||||
type JsExecResult struct {
|
||||
Output string `json:"output"` // 控制台输出
|
||||
Result string `json:"result"` // 返回值
|
||||
Error string `json:"error"` // 错误信息
|
||||
Duration string `json:"duration"` // 执行耗时
|
||||
Success bool `json:"success"` // 是否成功
|
||||
}
|
||||
|
||||
// consoleLog 存储 console.log 输出
|
||||
type consoleLogger struct {
|
||||
logs []string
|
||||
}
|
||||
|
||||
func (c *consoleLogger) log(args ...interface{}) {
|
||||
c.logs = append(c.logs, fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
func (c *consoleLogger) logFn(call goja.FunctionCall) goja.Value {
|
||||
var args []interface{}
|
||||
for _, arg := range call.Arguments {
|
||||
args = append(args, arg.Export())
|
||||
}
|
||||
c.logs = append(c.logs, fmt.Sprint(args...))
|
||||
return goja.Undefined()
|
||||
}
|
||||
|
||||
// ExecuteJs 执行 JavaScript 代码
|
||||
func ExecuteJs(code string) *JsExecResult {
|
||||
result := &JsExecResult{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
if strings.TrimSpace(code) == "" {
|
||||
result.Error = "代码不能为空"
|
||||
return result
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
// 创建 JS 虚拟机
|
||||
vm := goja.New()
|
||||
|
||||
// 创建日志记录器
|
||||
logger := &consoleLogger{
|
||||
logs: make([]string, 0),
|
||||
}
|
||||
|
||||
// 注入 console 对象
|
||||
consoleObj := vm.NewObject()
|
||||
consoleObj.Set("log", logger.logFn)
|
||||
consoleObj.Set("info", logger.logFn)
|
||||
consoleObj.Set("warn", logger.logFn)
|
||||
consoleObj.Set("error", logger.logFn)
|
||||
vm.Set("console", consoleObj)
|
||||
|
||||
// 注入常用全局函数
|
||||
vm.Set("parseInt", parseInt)
|
||||
vm.Set("parseFloat", parseFloat)
|
||||
vm.Set("isNaN", isNaN)
|
||||
vm.Set("isFinite", isFinite)
|
||||
vm.Set("String", vm.Get("String"))
|
||||
vm.Set("Number", vm.Get("Number"))
|
||||
vm.Set("Boolean", vm.Get("Boolean"))
|
||||
vm.Set("Array", vm.Get("Array"))
|
||||
vm.Set("Object", vm.Get("Object"))
|
||||
vm.Set("JSON", vm.Get("JSON"))
|
||||
vm.Set("Math", vm.Get("Math"))
|
||||
vm.Set("Date", vm.Get("Date"))
|
||||
vm.Set("RegExp", vm.Get("RegExp"))
|
||||
|
||||
// 执行代码
|
||||
val, err := vm.RunString(code)
|
||||
if err != nil {
|
||||
result.Error = err.Error()
|
||||
result.Duration = time.Since(start).String()
|
||||
result.Output = strings.Join(logger.logs, "\n")
|
||||
return result
|
||||
}
|
||||
|
||||
// 获取返回值
|
||||
resultVal := ""
|
||||
if val != nil && !goja.IsUndefined(val) && !goja.IsNull(val) {
|
||||
resultVal = fmt.Sprintf("%v", val.Export())
|
||||
}
|
||||
|
||||
result.Output = strings.Join(logger.logs, "\n")
|
||||
result.Result = resultVal
|
||||
result.Duration = time.Since(start).String()
|
||||
result.Success = true
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// parseInt 解析整数
|
||||
func parseInt(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) < 1 {
|
||||
return goja.NaN()
|
||||
}
|
||||
|
||||
arg := call.Arguments[0].String()
|
||||
var radix int64 = 10
|
||||
if len(call.Arguments) > 1 {
|
||||
radix = call.Arguments[1].ToInteger()
|
||||
}
|
||||
|
||||
var result int64
|
||||
var err error
|
||||
|
||||
switch radix {
|
||||
case 16:
|
||||
_, err = fmt.Sscanf(arg, "%x", &result)
|
||||
case 8:
|
||||
_, err = fmt.Sscanf(arg, "%o", &result)
|
||||
default:
|
||||
_, err = fmt.Sscanf(arg, "%d", &result)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return goja.NaN()
|
||||
}
|
||||
|
||||
return goja.New().ToValue(result)
|
||||
}
|
||||
|
||||
// parseFloat 解析浮点数
|
||||
func parseFloat(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) < 1 {
|
||||
return goja.NaN()
|
||||
}
|
||||
|
||||
arg := call.Arguments[0].String()
|
||||
var result float64
|
||||
_, err := fmt.Sscanf(arg, "%f", &result)
|
||||
if err != nil {
|
||||
return goja.NaN()
|
||||
}
|
||||
|
||||
return goja.New().ToValue(result)
|
||||
}
|
||||
|
||||
// isNaN 检查是否为 NaN
|
||||
func isNaN(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) < 1 {
|
||||
return goja.New().ToValue(true)
|
||||
}
|
||||
|
||||
val := call.Arguments[0].ToNumber()
|
||||
if goja.IsNaN(val) {
|
||||
return goja.New().ToValue(true)
|
||||
}
|
||||
return goja.New().ToValue(false)
|
||||
}
|
||||
|
||||
// isFinite 检查是否为有限数
|
||||
func isFinite(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) < 1 {
|
||||
return goja.New().ToValue(false)
|
||||
}
|
||||
|
||||
val := call.Arguments[0].ToNumber()
|
||||
if goja.IsNaN(val) || goja.IsInfinity(val) {
|
||||
return goja.New().ToValue(false)
|
||||
}
|
||||
return goja.New().ToValue(true)
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"webhelper/global"
|
||||
)
|
||||
|
||||
func GetLang() (*global.LanguagePack, error) {
|
||||
return global.GetLangPack()
|
||||
}
|
||||
|
||||
func GetALLLang() []global.LanguageInfo {
|
||||
return global.GetLangInfoList()
|
||||
}
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RequestLog 请求日志
|
||||
type RequestLog struct {
|
||||
ID int `json:"id"` // 序号
|
||||
Method string `json:"method"` // 请求方法
|
||||
URL string `json:"url"` // 请求地址
|
||||
Host string `json:"host"` // 主机地址
|
||||
Path string `json:"path"` // 请求路径
|
||||
Headers map[string]string `json:"headers"` // 请求头
|
||||
Cookies map[string]string `json:"cookies"` // Cookie
|
||||
BodyType string `json:"bodyType"` // 数据类型
|
||||
Body string `json:"body"` // 请求体
|
||||
StatusCode int `json:"statusCode"` // 状态码
|
||||
Status string `json:"status"` // 状态描述
|
||||
RespHeaders map[string]string `json:"respHeaders"` // 响应头
|
||||
RespBody string `json:"respBody"` // 响应正文
|
||||
RespSize int64 `json:"respSize"` // 响应大小
|
||||
Time string `json:"time"` // 请求耗时
|
||||
CreatedAt string `json:"createdAt"` // 创建时间
|
||||
}
|
||||
|
||||
// LogManager 日志管理器
|
||||
type LogManager struct {
|
||||
logDir string
|
||||
logs []*RequestLog
|
||||
maxLogs int
|
||||
fileName string
|
||||
}
|
||||
|
||||
var logManager *LogManager
|
||||
|
||||
// InitLogManager 初始化日志管理器
|
||||
func InitLogManager(logDir string) {
|
||||
logManager = &LogManager{
|
||||
logDir: logDir,
|
||||
logs: make([]*RequestLog, 0),
|
||||
maxLogs: 1000,
|
||||
fileName: "http_requests.json",
|
||||
}
|
||||
logManager.loadLogs()
|
||||
}
|
||||
|
||||
// GetLogManager 获取日志管理器
|
||||
func GetLogManager() *LogManager {
|
||||
if logManager == nil {
|
||||
InitLogManager("logs")
|
||||
}
|
||||
return logManager
|
||||
}
|
||||
|
||||
// loadLogs 从文件加载日志
|
||||
func (m *LogManager) loadLogs() {
|
||||
filePath := filepath.Join(m.logDir, m.fileName)
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
m.logs = make([]*RequestLog, 0)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var logs []*RequestLog
|
||||
if err := json.Unmarshal(data, &logs); err != nil {
|
||||
m.logs = make([]*RequestLog, 0)
|
||||
return
|
||||
}
|
||||
|
||||
m.logs = logs
|
||||
}
|
||||
|
||||
// saveLogs 保存日志到文件
|
||||
func (m *LogManager) saveLogs() error {
|
||||
if err := os.MkdirAll(m.logDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filePath := filepath.Join(m.logDir, m.fileName)
|
||||
data, err := json.MarshalIndent(m.logs, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(filePath, data, 0644)
|
||||
}
|
||||
|
||||
// AddLog 添加日志
|
||||
func (m *LogManager) AddLog(log *RequestLog) error {
|
||||
// 生成序号
|
||||
if len(m.logs) > 0 {
|
||||
log.ID = m.logs[0].ID + 1
|
||||
} else {
|
||||
log.ID = 1
|
||||
}
|
||||
|
||||
log.CreatedAt = time.Now().Format(time.DateTime)
|
||||
|
||||
// 插入到开头(最新的在前面)
|
||||
m.logs = append([]*RequestLog{log}, m.logs...)
|
||||
|
||||
// 限制最大日志数量
|
||||
if len(m.logs) > m.maxLogs {
|
||||
m.logs = m.logs[:m.maxLogs]
|
||||
}
|
||||
|
||||
return m.saveLogs()
|
||||
}
|
||||
|
||||
// GetLogs 获取所有日志
|
||||
func (m *LogManager) GetLogs() []*RequestLog {
|
||||
return m.logs
|
||||
}
|
||||
|
||||
// GetLog 获取单条日志
|
||||
func (m *LogManager) GetLog(id int) *RequestLog {
|
||||
for _, log := range m.logs {
|
||||
if log.ID == id {
|
||||
return log
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteLog 删除日志
|
||||
func (m *LogManager) DeleteLog(id int) error {
|
||||
for i, log := range m.logs {
|
||||
if log.ID == id {
|
||||
m.logs = append(m.logs[:i], m.logs[i+1:]...)
|
||||
return m.saveLogs()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearLogs 清空日志
|
||||
func (m *LogManager) ClearLogs() error {
|
||||
m.logs = make([]*RequestLog, 0)
|
||||
return m.saveLogs()
|
||||
}
|
||||
|
||||
// SearchLogs 搜索日志
|
||||
func (m *LogManager) SearchLogs(keyword string) []*RequestLog {
|
||||
var result []*RequestLog
|
||||
keyword = strings.ToLower(keyword)
|
||||
|
||||
for _, log := range m.logs {
|
||||
if strings.Contains(strings.ToLower(log.URL), keyword) ||
|
||||
strings.Contains(strings.ToLower(log.Method), keyword) ||
|
||||
strings.Contains(strings.ToLower(log.Host), keyword) ||
|
||||
strings.Contains(strings.ToLower(log.Path), keyword) {
|
||||
result = append(result, log)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// CreateLogFromResponse 从响应创建日志
|
||||
func CreateLogFromResponse(req *HttpRequest, resp *HttpResponse) *RequestLog {
|
||||
// 解析 URL
|
||||
host := ""
|
||||
path := req.URL
|
||||
if strings.HasPrefix(req.URL, "http://") {
|
||||
parts := strings.SplitN(strings.TrimPrefix(req.URL, "http://"), "/", 2)
|
||||
host = parts[0]
|
||||
if len(parts) > 1 {
|
||||
path = "/" + parts[1]
|
||||
}
|
||||
} else if strings.HasPrefix(req.URL, "https://") {
|
||||
parts := strings.SplitN(strings.TrimPrefix(req.URL, "https://"), "/", 2)
|
||||
host = parts[0]
|
||||
if len(parts) > 1 {
|
||||
path = "/" + parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
return &RequestLog{
|
||||
Method: req.Method,
|
||||
URL: req.URL,
|
||||
Host: host,
|
||||
Path: path,
|
||||
Headers: req.Headers,
|
||||
Cookies: req.Cookies,
|
||||
BodyType: req.BodyType,
|
||||
Body: req.Body,
|
||||
StatusCode: resp.StatusCode,
|
||||
Status: resp.Status,
|
||||
RespHeaders: resp.Headers,
|
||||
RespBody: resp.Body,
|
||||
RespSize: resp.Size,
|
||||
Time: resp.Time,
|
||||
}
|
||||
}
|
||||
|
||||
// FormatFileSize 格式化文件大小
|
||||
func FormatFileSize(size int64) string {
|
||||
if size < 1024 {
|
||||
return fmt.Sprintf("%d B", size)
|
||||
} else if size < 1024*1024 {
|
||||
return fmt.Sprintf("%.2f KB", float64(size)/1024)
|
||||
} else if size < 1024*1024*1024 {
|
||||
return fmt.Sprintf("%.2f MB", float64(size)/(1024*1024))
|
||||
}
|
||||
return fmt.Sprintf("%.2f GB", float64(size)/(1024*1024*1024))
|
||||
}
|
||||
|
||||
// SortLogsByTime 按时间排序日志
|
||||
func SortLogsByTime(logs []*RequestLog, desc bool) []*RequestLog {
|
||||
sorted := make([]*RequestLog, len(logs))
|
||||
copy(sorted, logs)
|
||||
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
timeI, _ := time.Parse(time.DateTime, sorted[i].CreatedAt)
|
||||
timeJ, _ := time.Parse(time.DateTime, sorted[j].CreatedAt)
|
||||
if desc {
|
||||
return timeI.After(timeJ)
|
||||
}
|
||||
return timeI.Before(timeJ)
|
||||
})
|
||||
|
||||
return sorted
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"webhelper/global"
|
||||
)
|
||||
|
||||
func LogInfo(msg string) {
|
||||
global.Log.Info(msg)
|
||||
}
|
||||
|
||||
func LogWarn(msg string) {
|
||||
global.Log.Warn(msg)
|
||||
}
|
||||
|
||||
func LogError(msg string) {
|
||||
global.Log.Error(msg)
|
||||
}
|
||||
|
||||
func LogDebug(msg string) {
|
||||
global.Log.Debug(msg)
|
||||
}
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"webhelper/global"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// WebSocketClient WebSocket 客户端
|
||||
type WebSocketClient struct {
|
||||
conn *websocket.Conn
|
||||
isConnected bool
|
||||
mu sync.RWMutex
|
||||
onMessage func(message string)
|
||||
onError func(err error)
|
||||
onClose func()
|
||||
}
|
||||
|
||||
// WebSocketConfig WebSocket 配置
|
||||
type WebSocketConfig struct {
|
||||
URL string `json:"url"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
Insecure bool `json:"insecure"`
|
||||
}
|
||||
|
||||
// WebSocketMessage WebSocket 消息
|
||||
type WebSocketMessage struct {
|
||||
Type string `json:"type"` // text, binary, ping, pong, error, system
|
||||
Content string `json:"content"`
|
||||
Time string `json:"time"`
|
||||
}
|
||||
|
||||
var wsClient *WebSocketClient
|
||||
|
||||
// NewWebSocketClient 创建 WebSocket 客户端
|
||||
func NewWebSocketClient() *WebSocketClient {
|
||||
return &WebSocketClient{
|
||||
isConnected: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Connect 连接 WebSocket
|
||||
func (c *WebSocketClient) Connect(config *WebSocketConfig, onMessage func(string), onError func(error), onClose func()) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.isConnected {
|
||||
return fmt.Errorf("已连接")
|
||||
}
|
||||
|
||||
// 解析 URL
|
||||
u, err := url.Parse(config.URL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("URL 格式错误: %v", err)
|
||||
}
|
||||
|
||||
// 创建 HTTP Header(只添加用户自定义头)
|
||||
header := http.Header{}
|
||||
for key, value := range config.Headers {
|
||||
header.Set(key, value)
|
||||
}
|
||||
|
||||
// 创建 Dialer(最简配置)
|
||||
dialer := &websocket.Dialer{
|
||||
HandshakeTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
// 连接
|
||||
global.Log.Infof("正在连接 WebSocket: %s", u.String())
|
||||
conn, _, err := dialer.Dial(u.String(), header)
|
||||
if err != nil {
|
||||
return fmt.Errorf("连接失败: %v", err)
|
||||
}
|
||||
|
||||
c.conn = conn
|
||||
c.isConnected = true
|
||||
c.onMessage = onMessage
|
||||
c.onError = onError
|
||||
c.onClose = onClose
|
||||
|
||||
global.Log.Infof("WebSocket 连接成功: %s", u.String())
|
||||
|
||||
// 启动消息监听
|
||||
go c.readMessages()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// readMessages 读取消息
|
||||
func (c *WebSocketClient) readMessages() {
|
||||
defer func() {
|
||||
c.mu.Lock()
|
||||
c.isConnected = false
|
||||
c.mu.Unlock()
|
||||
if c.onClose != nil {
|
||||
c.onClose()
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
_, message, err := c.conn.ReadMessage()
|
||||
if err != nil {
|
||||
if c.onError != nil {
|
||||
c.onError(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if c.onMessage != nil {
|
||||
c.onMessage(string(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send 发送消息
|
||||
func (c *WebSocketClient) Send(message string) error {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if !c.isConnected || c.conn == nil {
|
||||
return fmt.Errorf("未连接")
|
||||
}
|
||||
|
||||
return c.conn.WriteMessage(websocket.TextMessage, []byte(message))
|
||||
}
|
||||
|
||||
// Close 关闭连接
|
||||
func (c *WebSocketClient) Close() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.conn != nil {
|
||||
err := c.conn.Close()
|
||||
c.isConnected = false
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsConnected 是否已连接
|
||||
func (c *WebSocketClient) IsConnected() bool {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.isConnected
|
||||
}
|
||||
|
||||
// GetWebSocketClient 获取 WebSocket 客户端
|
||||
func GetWebSocketClient() *WebSocketClient {
|
||||
if wsClient == nil {
|
||||
wsClient = NewWebSocketClient()
|
||||
}
|
||||
return wsClient
|
||||
}
|
||||
|
||||
// ConnectWebSocket 连接 WebSocket
|
||||
func ConnectWebSocket(config *WebSocketConfig, onMessage func(string), onError func(error), onClose func()) error {
|
||||
client := GetWebSocketClient()
|
||||
return client.Connect(config, onMessage, onError, onClose)
|
||||
}
|
||||
|
||||
// SendWebSocketMessage 发送 WebSocket 消息
|
||||
func SendWebSocketMessage(message string) error {
|
||||
client := GetWebSocketClient()
|
||||
return client.Send(message)
|
||||
}
|
||||
|
||||
// CloseWebSocket 关闭 WebSocket 连接
|
||||
func CloseWebSocket() error {
|
||||
client := GetWebSocketClient()
|
||||
return client.Close()
|
||||
}
|
||||
|
||||
// IsWebSocketConnected WebSocket 是否已连接
|
||||
func IsWebSocketConnected() bool {
|
||||
client := GetWebSocketClient()
|
||||
return client.IsConnected()
|
||||
}
|
||||
|
||||
// TCPClient TCP 客户端
|
||||
type TCPClient struct {
|
||||
conn net.Conn
|
||||
isConnected bool
|
||||
mu sync.RWMutex
|
||||
onMessage func(message string)
|
||||
onError func(err error)
|
||||
onClose func()
|
||||
}
|
||||
|
||||
// TCPConfig TCP 配置
|
||||
type TCPConfig struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
}
|
||||
|
||||
// TCPMessage TCP 消息
|
||||
type TCPMessage struct {
|
||||
Type string `json:"type"`
|
||||
Content string `json:"content"`
|
||||
Time string `json:"time"`
|
||||
}
|
||||
|
||||
var tcpClient *TCPClient
|
||||
|
||||
// NewTCPClient 创建 TCP 客户端
|
||||
func NewTCPClient() *TCPClient {
|
||||
return &TCPClient{
|
||||
isConnected: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Connect 连接 TCP
|
||||
func (c *TCPClient) Connect(config *TCPConfig, onMessage func(string), onError func(error), onClose func()) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.isConnected {
|
||||
return fmt.Errorf("已连接")
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", config.Host, config.Port)
|
||||
conn, err := net.DialTimeout("tcp", addr, 10*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("连接失败: %v", err)
|
||||
}
|
||||
|
||||
c.conn = conn
|
||||
c.isConnected = true
|
||||
c.onMessage = onMessage
|
||||
c.onError = onError
|
||||
c.onClose = onClose
|
||||
|
||||
// 启动消息监听
|
||||
go c.readMessages()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// readMessages 读取消息
|
||||
func (c *TCPClient) readMessages() {
|
||||
defer func() {
|
||||
c.mu.Lock()
|
||||
c.isConnected = false
|
||||
c.mu.Unlock()
|
||||
if c.onClose != nil {
|
||||
c.onClose()
|
||||
}
|
||||
}()
|
||||
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
n, err := c.conn.Read(buf)
|
||||
if err != nil {
|
||||
if c.onError != nil {
|
||||
c.onError(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if c.onMessage != nil {
|
||||
c.onMessage(string(buf[:n]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send 发送消息
|
||||
func (c *TCPClient) Send(message string) error {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if !c.isConnected || c.conn == nil {
|
||||
return fmt.Errorf("未连接")
|
||||
}
|
||||
|
||||
_, err := c.conn.Write([]byte(message))
|
||||
return err
|
||||
}
|
||||
|
||||
// Close 关闭连接
|
||||
func (c *TCPClient) Close() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.conn != nil {
|
||||
err := c.conn.Close()
|
||||
c.isConnected = false
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsConnected 是否已连接
|
||||
func (c *TCPClient) IsConnected() bool {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.isConnected
|
||||
}
|
||||
|
||||
// GetTCPClient 获取 TCP 客户端
|
||||
func GetTCPClient() *TCPClient {
|
||||
if tcpClient == nil {
|
||||
tcpClient = NewTCPClient()
|
||||
}
|
||||
return tcpClient
|
||||
}
|
||||
|
||||
// ConnectTCP 连接 TCP
|
||||
func ConnectTCP(config *TCPConfig, onMessage func(string), onError func(error), onClose func()) error {
|
||||
client := GetTCPClient()
|
||||
return client.Connect(config, onMessage, onError, onClose)
|
||||
}
|
||||
|
||||
// SendTCPMessage 发送 TCP 消息
|
||||
func SendTCPMessage(message string) error {
|
||||
client := GetTCPClient()
|
||||
return client.Send(message)
|
||||
}
|
||||
|
||||
// CloseTCP 关闭 TCP 连接
|
||||
func CloseTCP() error {
|
||||
client := GetTCPClient()
|
||||
return client.Close()
|
||||
}
|
||||
|
||||
// IsTCPConnected TCP 是否已连接
|
||||
func IsTCPConnected() bool {
|
||||
client := GetTCPClient()
|
||||
return client.isConnected
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>{{.Info.ProductName}}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>{{.OutputFilename}}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.wails.{{.Name}}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{{.Info.ProductVersion}}</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>{{.Info.Comments}}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{{.Info.ProductVersion}}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>iconfile</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.13.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>{{.Info.Copyright}}</string>
|
||||
{{if .Info.FileAssociations}}
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
{{range .Info.FileAssociations}}
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>{{.Ext}}</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>{{.Name}}</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>{{.Role}}</string>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>{{.IconName}}</string>
|
||||
</dict>
|
||||
{{end}}
|
||||
</array>
|
||||
{{end}}
|
||||
{{if .Info.Protocols}}
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
{{range .Info.Protocols}}
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.wails.{{.Scheme}}</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>{{.Scheme}}</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>{{.Role}}</string>
|
||||
</dict>
|
||||
{{end}}
|
||||
</array>
|
||||
{{end}}
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,63 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>{{.Info.ProductName}}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>{{.OutputFilename}}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.wails.{{.Name}}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{{.Info.ProductVersion}}</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>{{.Info.Comments}}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{{.Info.ProductVersion}}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>iconfile</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.13.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>{{.Info.Copyright}}</string>
|
||||
{{if .Info.FileAssociations}}
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
{{range .Info.FileAssociations}}
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>{{.Ext}}</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>{{.Name}}</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>{{.Role}}</string>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>{{.IconName}}</string>
|
||||
</dict>
|
||||
{{end}}
|
||||
</array>
|
||||
{{end}}
|
||||
{{if .Info.Protocols}}
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
{{range .Info.Protocols}}
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.wails.{{.Scheme}}</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>{{.Scheme}}</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>{{.Role}}</string>
|
||||
</dict>
|
||||
{{end}}
|
||||
</array>
|
||||
{{end}}
|
||||
</dict>
|
||||
</plist>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 225 KiB |
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"fixed": {
|
||||
"file_version": "{{.Info.ProductVersion}}"
|
||||
},
|
||||
"info": {
|
||||
"0000": {
|
||||
"ProductVersion": "{{.Info.ProductVersion}}",
|
||||
"CompanyName": "{{.Info.CompanyName}}",
|
||||
"FileDescription": "{{.Info.ProductName}}",
|
||||
"LegalCopyright": "{{.Info.Copyright}}",
|
||||
"ProductName": "{{.Info.ProductName}}",
|
||||
"Comments": "{{.Info.Comments}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
Unicode true
|
||||
|
||||
####
|
||||
## Please note: Template replacements don't work in this file. They are provided with default defines like
|
||||
## mentioned underneath.
|
||||
## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
|
||||
## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
|
||||
## from outside of Wails for debugging and development of the installer.
|
||||
##
|
||||
## For development first make a wails nsis build to populate the "wails_tools.nsh":
|
||||
## > wails build --target windows/amd64 --nsis
|
||||
## Then you can call makensis on this file with specifying the path to your binary:
|
||||
## For a AMD64 only installer:
|
||||
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
|
||||
## For a ARM64 only installer:
|
||||
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
|
||||
## For a installer with both architectures:
|
||||
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
|
||||
####
|
||||
## The following information is taken from the ProjectInfo file, but they can be overwritten here.
|
||||
####
|
||||
## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
|
||||
## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
|
||||
## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
|
||||
## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
|
||||
## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
|
||||
###
|
||||
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
|
||||
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||
####
|
||||
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
|
||||
####
|
||||
## Include the wails tools
|
||||
####
|
||||
!include "wails_tools.nsh"
|
||||
|
||||
# The version information for this two must consist of 4 parts
|
||||
VIProductVersion "${INFO_PRODUCTVERSION}.0"
|
||||
VIFileVersion "${INFO_PRODUCTVERSION}.0"
|
||||
|
||||
VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
|
||||
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
|
||||
VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
|
||||
VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
|
||||
VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
|
||||
VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
|
||||
|
||||
# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
|
||||
ManifestDPIAware true
|
||||
|
||||
!include "MUI.nsh"
|
||||
|
||||
!define MUI_ICON "..\icon.ico"
|
||||
!define MUI_UNICON "..\icon.ico"
|
||||
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
|
||||
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
|
||||
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
|
||||
|
||||
!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
|
||||
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
|
||||
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
|
||||
!insertmacro MUI_PAGE_INSTFILES # Installing page.
|
||||
!insertmacro MUI_PAGE_FINISH # Finished installation page.
|
||||
|
||||
!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
|
||||
|
||||
!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
|
||||
|
||||
## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
|
||||
#!uninstfinalize 'signtool --file "%1"'
|
||||
#!finalize 'signtool --file "%1"'
|
||||
|
||||
Name "${INFO_PRODUCTNAME}"
|
||||
OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
|
||||
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
|
||||
ShowInstDetails show # This will always show the installation details.
|
||||
|
||||
Function .onInit
|
||||
!insertmacro wails.checkArchitecture
|
||||
FunctionEnd
|
||||
|
||||
Section
|
||||
!insertmacro wails.setShellContext
|
||||
|
||||
!insertmacro wails.webview2runtime
|
||||
|
||||
SetOutPath $INSTDIR
|
||||
|
||||
!insertmacro wails.files
|
||||
|
||||
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
|
||||
!insertmacro wails.associateFiles
|
||||
!insertmacro wails.associateCustomProtocols
|
||||
|
||||
!insertmacro wails.writeUninstaller
|
||||
SectionEnd
|
||||
|
||||
Section "uninstall"
|
||||
!insertmacro wails.setShellContext
|
||||
|
||||
RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
|
||||
|
||||
RMDir /r $INSTDIR
|
||||
|
||||
Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
|
||||
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
|
||||
|
||||
!insertmacro wails.unassociateFiles
|
||||
!insertmacro wails.unassociateCustomProtocols
|
||||
|
||||
!insertmacro wails.deleteUninstaller
|
||||
SectionEnd
|
||||
@@ -0,0 +1,249 @@
|
||||
# DO NOT EDIT - Generated automatically by `wails build`
|
||||
|
||||
!include "x64.nsh"
|
||||
!include "WinVer.nsh"
|
||||
!include "FileFunc.nsh"
|
||||
|
||||
!ifndef INFO_PROJECTNAME
|
||||
!define INFO_PROJECTNAME "{{.Name}}"
|
||||
!endif
|
||||
!ifndef INFO_COMPANYNAME
|
||||
!define INFO_COMPANYNAME "{{.Info.CompanyName}}"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTNAME
|
||||
!define INFO_PRODUCTNAME "{{.Info.ProductName}}"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTVERSION
|
||||
!define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
|
||||
!endif
|
||||
!ifndef INFO_COPYRIGHT
|
||||
!define INFO_COPYRIGHT "{{.Info.Copyright}}"
|
||||
!endif
|
||||
!ifndef PRODUCT_EXECUTABLE
|
||||
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
|
||||
!endif
|
||||
!ifndef UNINST_KEY_NAME
|
||||
!define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||
!endif
|
||||
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
|
||||
|
||||
!ifndef REQUEST_EXECUTION_LEVEL
|
||||
!define REQUEST_EXECUTION_LEVEL "admin"
|
||||
!endif
|
||||
|
||||
RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
|
||||
|
||||
!ifdef ARG_WAILS_AMD64_BINARY
|
||||
!define SUPPORTS_AMD64
|
||||
!endif
|
||||
|
||||
!ifdef ARG_WAILS_ARM64_BINARY
|
||||
!define SUPPORTS_ARM64
|
||||
!endif
|
||||
|
||||
!ifdef SUPPORTS_AMD64
|
||||
!ifdef SUPPORTS_ARM64
|
||||
!define ARCH "amd64_arm64"
|
||||
!else
|
||||
!define ARCH "amd64"
|
||||
!endif
|
||||
!else
|
||||
!ifdef SUPPORTS_ARM64
|
||||
!define ARCH "arm64"
|
||||
!else
|
||||
!error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
|
||||
!endif
|
||||
!endif
|
||||
|
||||
!macro wails.checkArchitecture
|
||||
!ifndef WAILS_WIN10_REQUIRED
|
||||
!define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
|
||||
!endif
|
||||
|
||||
!ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
|
||||
!define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
|
||||
!endif
|
||||
|
||||
${If} ${AtLeastWin10}
|
||||
!ifdef SUPPORTS_AMD64
|
||||
${if} ${IsNativeAMD64}
|
||||
Goto ok
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
!ifdef SUPPORTS_ARM64
|
||||
${if} ${IsNativeARM64}
|
||||
Goto ok
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
IfSilent silentArch notSilentArch
|
||||
silentArch:
|
||||
SetErrorLevel 65
|
||||
Abort
|
||||
notSilentArch:
|
||||
MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
|
||||
Quit
|
||||
${else}
|
||||
IfSilent silentWin notSilentWin
|
||||
silentWin:
|
||||
SetErrorLevel 64
|
||||
Abort
|
||||
notSilentWin:
|
||||
MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
|
||||
Quit
|
||||
${EndIf}
|
||||
|
||||
ok:
|
||||
!macroend
|
||||
|
||||
!macro wails.files
|
||||
!ifdef SUPPORTS_AMD64
|
||||
${if} ${IsNativeAMD64}
|
||||
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
!ifdef SUPPORTS_ARM64
|
||||
${if} ${IsNativeARM64}
|
||||
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
|
||||
${EndIf}
|
||||
!endif
|
||||
!macroend
|
||||
|
||||
!macro wails.writeUninstaller
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
|
||||
SetRegView 64
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
|
||||
|
||||
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
|
||||
IntFmt $0 "0x%08X" $0
|
||||
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
|
||||
!macroend
|
||||
|
||||
!macro wails.deleteUninstaller
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
|
||||
SetRegView 64
|
||||
DeleteRegKey HKLM "${UNINST_KEY}"
|
||||
!macroend
|
||||
|
||||
!macro wails.setShellContext
|
||||
${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
|
||||
SetShellVarContext all
|
||||
${else}
|
||||
SetShellVarContext current
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
# Install webview2 by launching the bootstrapper
|
||||
# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
|
||||
!macro wails.webview2runtime
|
||||
!ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
|
||||
!define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
|
||||
!endif
|
||||
|
||||
SetRegView 64
|
||||
# If the admin key exists and is not empty then webview2 is already installed
|
||||
ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||
${If} $0 != ""
|
||||
Goto ok
|
||||
${EndIf}
|
||||
|
||||
${If} ${REQUEST_EXECUTION_LEVEL} == "user"
|
||||
# If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
|
||||
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||
${If} $0 != ""
|
||||
Goto ok
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
||||
SetDetailsPrint both
|
||||
DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
|
||||
SetDetailsPrint listonly
|
||||
|
||||
InitPluginsDir
|
||||
CreateDirectory "$pluginsdir\webview2bootstrapper"
|
||||
SetOutPath "$pluginsdir\webview2bootstrapper"
|
||||
File "tmp\MicrosoftEdgeWebview2Setup.exe"
|
||||
ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
|
||||
|
||||
SetDetailsPrint both
|
||||
ok:
|
||||
!macroend
|
||||
|
||||
# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
|
||||
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
|
||||
; Backup the previously associated file class
|
||||
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
|
||||
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
|
||||
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
|
||||
!macroend
|
||||
|
||||
!macro APP_UNASSOCIATE EXT FILECLASS
|
||||
; Backup the previously associated file class
|
||||
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
|
||||
|
||||
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
|
||||
!macroend
|
||||
|
||||
!macro wails.associateFiles
|
||||
; Create file associations
|
||||
{{range .Info.FileAssociations}}
|
||||
!insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
|
||||
|
||||
File "..\{{.IconName}}.ico"
|
||||
{{end}}
|
||||
!macroend
|
||||
|
||||
!macro wails.unassociateFiles
|
||||
; Delete app associations
|
||||
{{range .Info.FileAssociations}}
|
||||
!insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
|
||||
|
||||
Delete "$INSTDIR\{{.IconName}}.ico"
|
||||
{{end}}
|
||||
!macroend
|
||||
|
||||
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
|
||||
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
|
||||
!macroend
|
||||
|
||||
!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
|
||||
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||
!macroend
|
||||
|
||||
!macro wails.associateCustomProtocols
|
||||
; Create custom protocols associations
|
||||
{{range .Info.Protocols}}
|
||||
!insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
|
||||
|
||||
{{end}}
|
||||
!macroend
|
||||
|
||||
!macro wails.unassociateCustomProtocols
|
||||
; Delete app custom protocol associations
|
||||
{{range .Info.Protocols}}
|
||||
!insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
|
||||
{{end}}
|
||||
!macroend
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</assembly>
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>App</title>
|
||||
</head>
|
||||
<body style="background-color: #ffffff;">
|
||||
<div id="app"></div>
|
||||
<script src="./src/main.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Generated
+1705
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wailsio/runtime": "^3.0.0-alpha.66",
|
||||
"element-plus": "^2.9.10",
|
||||
"vue": "^3.2.37"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^3.0.3",
|
||||
"unplugin-auto-import": "^19.2.0",
|
||||
"unplugin-vue-components": "^28.5.0",
|
||||
"vite": "^3.0.7"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
4e89b15208d82473f2aacb5ee7a39659
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@@ -0,0 +1,102 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from './composables/useI18n'
|
||||
import HeaderBar from './components/HeaderBar.vue'
|
||||
import WebDebug from './components/WebDebug.vue'
|
||||
import JsDebug from './components/JsDebug.vue'
|
||||
import JsonParser from './components/JsonParser.vue'
|
||||
import SocketDebug from './components/SocketDebug.vue'
|
||||
import Settings from './components/Settings.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const activeTab = ref('webdebug')
|
||||
|
||||
const tabs = [
|
||||
{ name: 'webdebug', key: 'app.tab.webdebug' },
|
||||
{ name: 'jsdebug', key: 'app.tab.jsdebug' },
|
||||
{ name: 'jsonparser', key: 'app.tab.jsonparser' },
|
||||
{ name: 'socketdebug', key: 'app.tab.socketdebug' },
|
||||
{ name: 'settings', key: 'app.tab.settings' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-layout">
|
||||
<HeaderBar />
|
||||
|
||||
<div class="tab-bar">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.name"
|
||||
:class="['tab-item', { active: activeTab === tab.name }]"
|
||||
@click="activeTab = tab.name"
|
||||
>
|
||||
{{ t(tab.key) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-content">
|
||||
<WebDebug v-show="activeTab === 'webdebug'" class="page-component hide-scrollbar" />
|
||||
<JsDebug v-show="activeTab === 'jsdebug'" class="page-component hide-scrollbar" />
|
||||
<JsonParser v-show="activeTab === 'jsonparser'" class="page-component hide-scrollbar" />
|
||||
<SocketDebug v-show="activeTab === 'socketdebug'" class="page-component hide-scrollbar" />
|
||||
<Settings v-show="activeTab === 'settings'" class="page-component hide-scrollbar" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.app-layout {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #f5f7fa;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
padding: 0 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
padding: 12px 24px;
|
||||
cursor: pointer;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab-item:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #409eff;
|
||||
border-bottom-color: #409eff;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.page-component {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
padding: 16px;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.page-component::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
@@ -0,0 +1,125 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useI18n } from '../composables/useI18n'
|
||||
import { Minus, FullScreen, Close } from '@element-plus/icons-vue'
|
||||
import { WindowMinimise, WindowToggleMaximise, WindowClose, GetVersion } from '../../wailsjs/go/service/App'
|
||||
|
||||
const { t } = useI18n()
|
||||
const version = ref('dev')
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
version.value = await GetVersion()
|
||||
} catch (e) {
|
||||
// 保持默认值
|
||||
}
|
||||
})
|
||||
|
||||
const handleDoubleClick = (e) => {
|
||||
if (e.target.closest('.right-panel')) {
|
||||
return
|
||||
}
|
||||
WindowToggleMaximise()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="top-bar" @dblclick="handleDoubleClick">
|
||||
<div class="left-panel">
|
||||
<img src="/appicon.png" alt="logo" class="app-icon" />
|
||||
<span class="app-title">{{ t('header.app_name') }}</span>
|
||||
<span class="app-version">{{ version }}</span>
|
||||
</div>
|
||||
|
||||
<div class="right-panel">
|
||||
<button type="button" class="window-btn" @click.stop="WindowMinimise">
|
||||
<el-icon><Minus /></el-icon>
|
||||
</button>
|
||||
<button type="button" class="window-btn" @click.stop="WindowToggleMaximise">
|
||||
<el-icon><FullScreen /></el-icon>
|
||||
</button>
|
||||
<button type="button" class="window-btn close-btn" @click.stop="WindowClose">
|
||||
<el-icon><Close /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.top-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
height: 32px;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
--wails-draggable: drag;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.app-version {
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.window-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: #606266;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.window-btn:hover {
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.window-btn:active {
|
||||
background-color: rgba(0, 0, 0, 0.12);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background-color: #f56c6c;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.close-btn:active {
|
||||
background-color: #e64242;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,188 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from '../composables/useI18n'
|
||||
import { ExecuteJs } from '../../wailsjs/go/service/App'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const code = ref(`// 在这里输入 JavaScript 代码
|
||||
function hello() {
|
||||
console.log('Hello, WebHelper!')
|
||||
return '执行完成'
|
||||
}
|
||||
|
||||
hello()`)
|
||||
|
||||
const output = ref('')
|
||||
const result = ref('')
|
||||
const duration = ref('')
|
||||
const success = ref(null)
|
||||
const running = ref(false)
|
||||
|
||||
const runCode = async () => {
|
||||
if (!code.value.trim()) {
|
||||
ElMessage.warning(t('jsdebug.msg.code_required'))
|
||||
return
|
||||
}
|
||||
|
||||
running.value = true
|
||||
output.value = ''
|
||||
result.value = ''
|
||||
duration.value = ''
|
||||
success.value = null
|
||||
|
||||
try {
|
||||
const res = await ExecuteJs(code.value)
|
||||
output.value = res.output || ''
|
||||
result.value = res.result || ''
|
||||
duration.value = res.duration || ''
|
||||
success.value = res.success
|
||||
|
||||
if (!res.success) {
|
||||
ElMessage.error(t('jsdebug.msg.execute_failed') + res.error)
|
||||
}
|
||||
} catch (e) {
|
||||
success.value = false
|
||||
ElMessage.error(t('jsdebug.msg.execute_failed') + e)
|
||||
} finally {
|
||||
running.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const clearAll = () => {
|
||||
code.value = ''
|
||||
output.value = ''
|
||||
result.value = ''
|
||||
duration.value = ''
|
||||
success.value = null
|
||||
}
|
||||
|
||||
const copyResult = async () => {
|
||||
const text = output.value + (result.value ? '\n' + t('jsdebug.msg.copy_prefix') + result.value : '')
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
ElMessage.success(t('jsdebug.msg.copied'))
|
||||
} catch (e) {
|
||||
ElMessage.error(t('jsdebug.msg.copy_failed'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="js-debug">
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<span class="toolbar-title">{{ t('jsdebug.title') }}</span>
|
||||
<el-tag v-if="duration" type="info" size="small">{{ t('jsdebug.tag.duration') }}{{ duration }}</el-tag>
|
||||
<el-tag v-if="success === true" type="success" size="small">{{ t('jsdebug.tag.success') }}</el-tag>
|
||||
<el-tag v-if="success === false" type="danger" size="small">{{ t('jsdebug.tag.failed') }}</el-tag>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-button type="primary" :loading="running" @click="runCode">{{ t('jsdebug.btn.run') }}</el-button>
|
||||
<el-button @click="clearAll">{{ t('jsdebug.btn.clear') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="code-section">
|
||||
<div class="section-label">{{ t('jsdebug.section.code') }}</div>
|
||||
<el-input
|
||||
v-model="code"
|
||||
type="textarea"
|
||||
:rows="16"
|
||||
:placeholder="t('jsdebug.code_placeholder')"
|
||||
class="code-editor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="output-section">
|
||||
<div class="section-label">
|
||||
<span>{{ t('jsdebug.section.output') }}</span>
|
||||
<el-button size="small" @click="copyResult" :disabled="!output && !result">{{ t('jsdebug.btn.copy') }}</el-button>
|
||||
</div>
|
||||
<el-input
|
||||
:value="output"
|
||||
type="textarea"
|
||||
:rows="8"
|
||||
readonly
|
||||
:placeholder="t('jsdebug.output_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="result-section">
|
||||
<div class="section-label">{{ t('jsdebug.section.return_value') }}</div>
|
||||
<el-input
|
||||
:value="result"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
readonly
|
||||
:placeholder="t('jsdebug.return_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.js-debug {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 16px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.toolbar-title {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.code-section {
|
||||
margin-bottom: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.output-section {
|
||||
margin-bottom: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.result-section {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.code-editor :deep(textarea) {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
background-color: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,526 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useI18n } from '../composables/useI18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const input = ref('')
|
||||
const parsedData = ref(null)
|
||||
const error = ref('')
|
||||
const viewMode = ref('tree') // tree, table, raw
|
||||
const expandedKeys = ref(new Set())
|
||||
const searchKeyword = ref('')
|
||||
|
||||
// 解析 JSON
|
||||
const parseJson = () => {
|
||||
error.value = ''
|
||||
parsedData.value = null
|
||||
|
||||
if (!input.value.trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
parsedData.value = JSON.parse(input.value)
|
||||
// 自动展开第一层
|
||||
expandFirstLevel()
|
||||
} catch (e) {
|
||||
error.value = t('jsonparser.msg.parse_error') + e.message
|
||||
}
|
||||
}
|
||||
|
||||
// 自动展开第一层
|
||||
const expandFirstLevel = () => {
|
||||
expandedKeys.value.clear()
|
||||
if (parsedData.value && typeof parsedData.value === 'object') {
|
||||
if (Array.isArray(parsedData.value)) {
|
||||
parsedData.value.forEach((_, index) => {
|
||||
expandedKeys.value.add(`root-${index}`)
|
||||
})
|
||||
} else {
|
||||
Object.keys(parsedData.value).forEach(key => {
|
||||
expandedKeys.value.add(`root-${key}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 切换展开/折叠
|
||||
const toggleExpand = (key) => {
|
||||
if (expandedKeys.value.has(key)) {
|
||||
expandedKeys.value.delete(key)
|
||||
} else {
|
||||
expandedKeys.value.add(key)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否展开
|
||||
const isExpanded = (key) => {
|
||||
return expandedKeys.value.has(key)
|
||||
}
|
||||
|
||||
// 获取值类型
|
||||
const getValueType = (value) => {
|
||||
if (value === null) return 'null'
|
||||
if (Array.isArray(value)) return 'array'
|
||||
return typeof value
|
||||
}
|
||||
|
||||
// 获取类型颜色
|
||||
const getTypeColor = (type) => {
|
||||
const colors = {
|
||||
'string': '#67c23a',
|
||||
'number': '#e6a23c',
|
||||
'boolean': '#409eff',
|
||||
'null': '#909399',
|
||||
'object': '#f56c6c',
|
||||
'array': '#9b59b6'
|
||||
}
|
||||
return colors[type] || '#909399'
|
||||
}
|
||||
|
||||
// 格式化值显示
|
||||
const formatValue = (value) => {
|
||||
if (value === null) return 'null'
|
||||
if (typeof value === 'string') return `"${value}"`
|
||||
return String(value)
|
||||
}
|
||||
|
||||
// 复制到剪贴板
|
||||
const copyToClipboard = async (text) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
ElMessage.success(t('jsonparser.msg.copied'))
|
||||
} catch (e) {
|
||||
ElMessage.error(t('jsonparser.msg.copy_failed'))
|
||||
}
|
||||
}
|
||||
|
||||
// 复制整个 JSON
|
||||
const copyJson = () => {
|
||||
if (parsedData.value) {
|
||||
copyToClipboard(JSON.stringify(parsedData.value, null, 2))
|
||||
}
|
||||
}
|
||||
|
||||
// 复制单个值
|
||||
const copyValue = (value) => {
|
||||
const text = typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)
|
||||
copyToClipboard(text)
|
||||
}
|
||||
|
||||
// 清空
|
||||
const clearAll = () => {
|
||||
input.value = ''
|
||||
parsedData.value = null
|
||||
error.value = ''
|
||||
expandedKeys.value.clear()
|
||||
}
|
||||
|
||||
// 统计信息
|
||||
const stats = computed(() => {
|
||||
if (!parsedData.value) return null
|
||||
|
||||
const count = {
|
||||
objects: 0,
|
||||
arrays: 0,
|
||||
strings: 0,
|
||||
numbers: 0,
|
||||
booleans: 0,
|
||||
nulls: 0
|
||||
}
|
||||
|
||||
const traverse = (value) => {
|
||||
if (value === null) {
|
||||
count.nulls++
|
||||
return
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
count.arrays++
|
||||
value.forEach(traverse)
|
||||
return
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
count.objects++
|
||||
Object.values(value).forEach(traverse)
|
||||
return
|
||||
}
|
||||
switch (typeof value) {
|
||||
case 'string': count.strings++; break
|
||||
case 'number': count.numbers++; break
|
||||
case 'boolean': count.booleans++; break
|
||||
}
|
||||
}
|
||||
|
||||
traverse(parsedData.value)
|
||||
return count
|
||||
})
|
||||
|
||||
// 监听输入变化
|
||||
watch(input, () => {
|
||||
if (input.value.trim()) {
|
||||
parseJson()
|
||||
} else {
|
||||
parsedData.value = null
|
||||
error.value = ''
|
||||
}
|
||||
})
|
||||
|
||||
// 监听从 WebDebug 发送过来的 JSON 数据
|
||||
const handleCopyToJson = (event) => {
|
||||
if (event.detail) {
|
||||
input.value = event.detail
|
||||
ElMessage.success(t('jsonparser.msg.received'))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('copy-to-json', handleCopyToJson)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('copy-to-json', handleCopyToJson)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="json-parser">
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<span class="toolbar-title">{{ t('jsonparser.title') }}</span>
|
||||
<template v-if="stats">
|
||||
<el-tag type="info" size="small">{{ t('jsonparser.stats.objects') }}{{ stats.objects }}</el-tag>
|
||||
<el-tag type="info" size="small">{{ t('jsonparser.stats.arrays') }}{{ stats.arrays }}</el-tag>
|
||||
<el-tag type="success" size="small">{{ t('jsonparser.stats.strings') }}{{ stats.strings }}</el-tag>
|
||||
<el-tag type="warning" size="small">{{ t('jsonparser.stats.numbers') }}{{ stats.numbers }}</el-tag>
|
||||
</template>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-radio-group v-model="viewMode" size="small">
|
||||
<el-radio-button label="tree">{{ t('jsonparser.view.tree') }}</el-radio-button>
|
||||
<el-radio-button label="table">{{ t('jsonparser.view.table') }}</el-radio-button>
|
||||
<el-radio-button label="raw">{{ t('jsonparser.view.raw') }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-button type="primary" @click="copyJson" :disabled="!parsedData">
|
||||
{{ t('jsonparser.btn.copy') }}
|
||||
</el-button>
|
||||
<el-button @click="clearAll">{{ t('jsonparser.btn.clear') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="parser-content">
|
||||
<div class="input-section">
|
||||
<div class="section-header">{{ t('jsonparser.section.input') }}</div>
|
||||
<el-input
|
||||
v-model="input"
|
||||
type="textarea"
|
||||
:rows="20"
|
||||
:placeholder="t('jsonparser.input_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="output-section">
|
||||
<div class="section-header">{{ t('jsonparser.section.result') }}</div>
|
||||
|
||||
<div v-if="viewMode === 'tree' && parsedData" class="tree-view">
|
||||
<div class="tree-node root">
|
||||
<div class="node-header" @click="toggleExpand('root')">
|
||||
<span class="expand-icon">{{ isExpanded('root') ? '▼' : '▶' }}</span>
|
||||
<span class="node-type" :style="{ color: getTypeColor(getValueType(parsedData)) }">
|
||||
{{ getValueType(parsedData) }}
|
||||
</span>
|
||||
<span class="node-info" v-if="Array.isArray(parsedData)">
|
||||
Array[{{ parsedData.length }}]
|
||||
</span>
|
||||
</div>
|
||||
<div class="node-children" v-if="isExpanded('root')">
|
||||
<template v-if="Array.isArray(parsedData)">
|
||||
<div v-for="(item, index) in parsedData" :key="index" class="tree-node">
|
||||
<div class="node-header" @click="toggleExpand(`root-${index}`)">
|
||||
<span class="expand-icon">{{ isExpanded(`root-${index}`) ? '▼' : '▶' }}</span>
|
||||
<span class="node-index">[{{ index }}]</span>
|
||||
<span class="node-type" :style="{ color: getTypeColor(getValueType(item)) }">
|
||||
{{ getValueType(item) }}
|
||||
</span>
|
||||
<span class="node-preview" v-if="typeof item !== 'object' || item === null">
|
||||
{{ formatValue(item) }}
|
||||
</span>
|
||||
<span class="node-preview" v-else-if="Array.isArray(item)">
|
||||
Array[{{ item.length }}]
|
||||
</span>
|
||||
<span class="node-preview" v-else>
|
||||
Object{ {{ Object.keys(item).length }} }
|
||||
</span>
|
||||
<el-button size="small" text @click.stop="copyValue(item)">{{ t('jsonparser.btn.copy') }}</el-button>
|
||||
</div>
|
||||
<div class="node-children" v-if="isExpanded(`root-${index}`) && typeof item === 'object' && item !== null">
|
||||
<template v-if="Array.isArray(item)">
|
||||
<div v-for="(subItem, subIndex) in item" :key="subIndex" class="tree-node leaf">
|
||||
<div class="node-header">
|
||||
<span class="node-index">[{{ subIndex }}]</span>
|
||||
<span class="node-type" :style="{ color: getTypeColor(getValueType(subItem)) }">
|
||||
{{ getValueType(subItem) }}
|
||||
</span>
|
||||
<span class="node-value">{{ formatValue(subItem) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-for="(subValue, subKey) in item" :key="subKey" class="tree-node leaf">
|
||||
<div class="node-header">
|
||||
<span class="node-key">{{ subKey }}:</span>
|
||||
<span class="node-type" :style="{ color: getTypeColor(getValueType(subValue)) }">
|
||||
{{ getValueType(subValue) }}
|
||||
</span>
|
||||
<span class="node-value">{{ formatValue(subValue) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-for="(value, key) in parsedData" :key="key" class="tree-node">
|
||||
<div class="node-header" @click="toggleExpand(`root-${key}`)">
|
||||
<span class="expand-icon">{{ isExpanded(`root-${key}`) ? '▼' : '▶' }}</span>
|
||||
<span class="node-key">{{ key }}:</span>
|
||||
<span class="node-type" :style="{ color: getTypeColor(getValueType(value)) }">
|
||||
{{ getValueType(value) }}
|
||||
</span>
|
||||
<span class="node-preview" v-if="typeof value !== 'object' || value === null">
|
||||
{{ formatValue(value) }}
|
||||
</span>
|
||||
<span class="node-preview" v-else-if="Array.isArray(value)">
|
||||
Array[{{ value.length }}]
|
||||
</span>
|
||||
<span class="node-preview" v-else>
|
||||
Object{ {{ Object.keys(value).length }} }
|
||||
</span>
|
||||
<el-button size="small" text @click.stop="copyValue(value)">{{ t('jsonparser.btn.copy') }}</el-button>
|
||||
</div>
|
||||
<div class="node-children" v-if="isExpanded(`root-${key}`) && typeof value === 'object' && value !== null">
|
||||
<template v-if="Array.isArray(value)">
|
||||
<div v-for="(subItem, subIndex) in value" :key="subIndex" class="tree-node leaf">
|
||||
<div class="node-header">
|
||||
<span class="node-index">[{{ subIndex }}]</span>
|
||||
<span class="node-type" :style="{ color: getTypeColor(getValueType(subItem)) }">
|
||||
{{ getValueType(subItem) }}
|
||||
</span>
|
||||
<span class="node-value">{{ formatValue(subItem) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-for="(subValue, subKey) in value" :key="subKey" class="tree-node leaf">
|
||||
<div class="node-header">
|
||||
<span class="node-key">{{ subKey }}:</span>
|
||||
<span class="node-type" :style="{ color: getTypeColor(getValueType(subValue)) }">
|
||||
{{ getValueType(subValue) }}
|
||||
</span>
|
||||
<span class="node-value">{{ formatValue(subValue) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="viewMode === 'table' && parsedData" class="table-view">
|
||||
<el-table :data="Array.isArray(parsedData) ? parsedData.map((item, index) => ({ _index: index, _value: item })) : Object.entries(parsedData).map(([key, value]) => ({ _key: key, _value: value }))" border stripe max-height="500">
|
||||
<el-table-column v-if="!Array.isArray(parsedData)" prop="_key" :label="t('jsonparser.table.column.key')" width="200" />
|
||||
<el-table-column v-else prop="_index" :label="t('jsonparser.table.column.index')" width="100" />
|
||||
<el-table-column prop="_value" :label="t('jsonparser.table.column.value')">
|
||||
<template #default="{ row }">
|
||||
<div class="table-value">
|
||||
<el-tag :color="getTypeColor(getValueType(row._value))" effect="dark" size="small" style="margin-right: 8px">
|
||||
{{ getValueType(row._value) }}
|
||||
</el-tag>
|
||||
<span>{{ typeof row._value === 'object' ? JSON.stringify(row._value) : formatValue(row._value) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('jsonparser.table.column.action')" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" text @click="copyValue(row._value)">{{ t('jsonparser.btn.copy') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div v-if="viewMode === 'raw'" class="raw-view">
|
||||
<el-input
|
||||
:value="parsedData ? JSON.stringify(parsedData, null, 2) : ''"
|
||||
type="textarea"
|
||||
:rows="20"
|
||||
readonly
|
||||
:placeholder="t('jsonparser.raw_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="!parsedData && !error && viewMode !== 'raw'" :description="t('jsonparser.empty')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-message">
|
||||
<el-alert :title="error" type="error" show-icon :closable="false" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.json-parser {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.toolbar-title {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.parser-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.input-section,
|
||||
.output-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
/* 树形视图样式 */
|
||||
.tree-view {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.tree-node.root {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.tree-node.leaf {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.node-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.node-header:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
width: 16px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.node-type {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding: 2px 6px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.node-key {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.node-index {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.node-preview {
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.node-value {
|
||||
color: #606266;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.node-children {
|
||||
margin-left: 20px;
|
||||
border-left: 1px dashed #dcdfe6;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
/* 表格视图样式 */
|
||||
.table-view {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.table-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* 原始视图样式 */
|
||||
.raw-view {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.raw-view :deep(textarea) {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="lang-selector">
|
||||
<el-select v-model="currentLang" placeholder="选择语言" @change="handleLangChange">
|
||||
<el-option
|
||||
v-for="lang in languages"
|
||||
:key="lang.code"
|
||||
:label="lang.name"
|
||||
:value="lang.code"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { GetALLLang, SetLanguage, GetCurrentLang, GetLangTextMap } from '../../wailsjs/go/service/App'
|
||||
import { useI18n } from '../composables/useI18n'
|
||||
|
||||
const { textMap } = useI18n()
|
||||
const currentLang = ref('')
|
||||
const languages = ref([])
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const langInfos = await GetALLLang()
|
||||
languages.value = langInfos.map(info => ({
|
||||
code: info.language_code,
|
||||
name: info.language_name
|
||||
}))
|
||||
currentLang.value = await GetCurrentLang() || 'zh-CN'
|
||||
} catch (error) {
|
||||
console.error('获取语言列表失败:', error)
|
||||
}
|
||||
})
|
||||
|
||||
const handleLangChange = async (langCode) => {
|
||||
try {
|
||||
await SetLanguage(langCode)
|
||||
// 重新加载语言映射,无需刷新页面
|
||||
const map = await GetLangTextMap()
|
||||
textMap.value = map || {}
|
||||
} catch (error) {
|
||||
console.error('设置语言失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.lang-selector {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,122 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useI18n } from '../composables/useI18n'
|
||||
import { GetALLLang, GetCurrentLang, SetLanguage, GetLangTextMap } from '../../wailsjs/go/service/App'
|
||||
|
||||
const { t, textMap } = useI18n()
|
||||
|
||||
const settings = ref({
|
||||
proxyEnabled: false,
|
||||
proxyHost: '',
|
||||
proxyPort: 7890
|
||||
})
|
||||
|
||||
const languages = ref([])
|
||||
const currentLang = ref('')
|
||||
|
||||
// 加载语言列表
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const langInfos = await GetALLLang()
|
||||
languages.value = langInfos.map(info => ({
|
||||
code: info.language_code,
|
||||
name: info.language_name
|
||||
}))
|
||||
currentLang.value = await GetCurrentLang() || 'zh-CN'
|
||||
} catch (e) {
|
||||
console.error('获取语言列表失败:', e)
|
||||
}
|
||||
})
|
||||
|
||||
// 切换语言
|
||||
const handleLangChange = async (langCode) => {
|
||||
try {
|
||||
await SetLanguage(langCode)
|
||||
const map = await GetLangTextMap()
|
||||
textMap.value = map || {}
|
||||
ElMessage.success(t('settings.msg.saved'))
|
||||
} catch (e) {
|
||||
console.error('设置语言失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
const saveSettings = () => {
|
||||
localStorage.setItem('webhelper-settings', JSON.stringify(settings.value))
|
||||
ElMessage.success(t('settings.msg.saved'))
|
||||
}
|
||||
|
||||
const resetSettings = () => {
|
||||
settings.value = {
|
||||
proxyEnabled: false,
|
||||
proxyHost: '',
|
||||
proxyPort: 7890
|
||||
}
|
||||
localStorage.removeItem('webhelper-settings')
|
||||
ElMessage.info(t('settings.msg.reset'))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings">
|
||||
<div class="settings-header">
|
||||
<span>{{ t('settings.title') }}</span>
|
||||
</div>
|
||||
|
||||
<el-form :model="settings" label-width="100px" class="settings-form">
|
||||
<el-form-item :label="t('settings.form.language_label')">
|
||||
<el-select v-model="currentLang" style="width: 200px" @change="handleLangChange">
|
||||
<el-option
|
||||
v-for="lang in languages"
|
||||
:key="lang.code"
|
||||
:label="lang.name"
|
||||
:value="lang.code"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">{{ t('settings.form.proxy_divider') }}</el-divider>
|
||||
|
||||
<el-form-item :label="t('settings.form.enable_proxy_label')">
|
||||
<el-switch v-model="settings.proxyEnabled" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="settings.proxyEnabled" :label="t('settings.form.proxy_address_label')">
|
||||
<el-input v-model="settings.proxyHost" :placeholder="t('settings.form.proxy_address_placeholder')" style="width: 200px" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="settings.proxyEnabled" :label="t('settings.form.proxy_port_label')">
|
||||
<el-input-number v-model="settings.proxyPort" :min="1" :max="65535" />
|
||||
</el-form-item>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveSettings">{{ t('settings.form.btn.save') }}</el-button>
|
||||
<el-button @click="resetSettings">{{ t('settings.form.btn.reset') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.settings {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
max-width: 600px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,462 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
|
||||
import { useI18n } from '../composables/useI18n'
|
||||
import {
|
||||
ConnectSocket,
|
||||
SendSocketMessage,
|
||||
DisconnectSocket,
|
||||
IsSocketConnected
|
||||
} from '../../wailsjs/go/service/App'
|
||||
import { EventsOn, EventsOff } from '../../wailsjs/runtime/runtime'
|
||||
import { Connection, SwitchButton, Promotion, Delete } from '@element-plus/icons-vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const protocol = ref('ws')
|
||||
const host = ref('localhost')
|
||||
const port = ref(8080)
|
||||
const path = ref('/')
|
||||
const headers = ref('')
|
||||
const message = ref('')
|
||||
const messages = ref([])
|
||||
const connected = ref(false)
|
||||
const connecting = ref(false)
|
||||
const autoScroll = ref(true)
|
||||
const messageListHeight = ref(0)
|
||||
|
||||
// 模板引用
|
||||
const messageListRef = ref(null)
|
||||
|
||||
const protocols = [
|
||||
{ label: 'WebSocket (ws://)', value: 'ws' },
|
||||
{ label: 'WebSocket Secure (wss://)', value: 'wss' },
|
||||
{ label: 'TCP', value: 'tcp' }
|
||||
]
|
||||
|
||||
// 获取当前时间
|
||||
const getTime = () => {
|
||||
return new Date().toLocaleTimeString()
|
||||
}
|
||||
|
||||
// 添加消息
|
||||
const addMessage = (type, content) => {
|
||||
messages.value.push({
|
||||
type,
|
||||
content,
|
||||
time: getTime()
|
||||
})
|
||||
|
||||
// 自动滚动到底部
|
||||
if (autoScroll.value) {
|
||||
nextTick(() => {
|
||||
scrollToBottom()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = () => {
|
||||
if (messageListRef.value) {
|
||||
const list = messageListRef.value
|
||||
list.scrollTop = list.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
// 处理滚动事件
|
||||
const handleScroll = (e) => {
|
||||
// 可用于检测用户是否滚动到顶部/底部
|
||||
}
|
||||
|
||||
// 处理端口输入,只允许数字
|
||||
const handlePortInput = (value) => {
|
||||
const numStr = value.replace(/\D/g, '')
|
||||
const num = parseInt(numStr)
|
||||
if (numStr === '') {
|
||||
port.value = ''
|
||||
} else if (num > 65535) {
|
||||
port.value = 65535
|
||||
} else {
|
||||
port.value = num
|
||||
}
|
||||
}
|
||||
|
||||
// 连接
|
||||
const connect = async () => {
|
||||
if (!host.value) {
|
||||
ElMessage.warning(t('socket.msg.host_required'))
|
||||
return
|
||||
}
|
||||
|
||||
const portNum = parseInt(port.value)
|
||||
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
||||
ElMessage.warning(t('socket.msg.port_invalid'))
|
||||
return
|
||||
}
|
||||
|
||||
connecting.value = true
|
||||
|
||||
try {
|
||||
const result = await ConnectSocket(
|
||||
protocol.value,
|
||||
host.value,
|
||||
portNum,
|
||||
path.value,
|
||||
headers.value
|
||||
)
|
||||
} catch (e) {
|
||||
addMessage('error', t('socket.msg.connect_failed') + e)
|
||||
} finally {
|
||||
connecting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 断开连接
|
||||
const disconnect = async () => {
|
||||
try {
|
||||
await DisconnectSocket(protocol.value)
|
||||
connected.value = false
|
||||
} catch (e) {
|
||||
addMessage('error', t('socket.msg.disconnect_failed') + e)
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
const send = async () => {
|
||||
if (!message.value.trim()) return
|
||||
if (!connected.value) {
|
||||
ElMessage.warning(t('socket.msg.not_connected'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await SendSocketMessage(protocol.value, message.value)
|
||||
if (result) {
|
||||
addMessage('sent', message.value)
|
||||
message.value = ''
|
||||
}
|
||||
} catch (e) {
|
||||
addMessage('error', t('socket.msg.send_failed') + e)
|
||||
}
|
||||
}
|
||||
|
||||
// 清空消息
|
||||
const clearMessages = () => {
|
||||
messages.value = []
|
||||
}
|
||||
|
||||
// 监听 Socket 事件
|
||||
const setupEventListeners = () => {
|
||||
EventsOn('socket-message', (data) => {
|
||||
addMessage(data.type, data.content)
|
||||
})
|
||||
|
||||
EventsOn('socket-status', (data) => {
|
||||
if (data.type === 'connected') {
|
||||
connected.value = true
|
||||
addMessage('system', data.content)
|
||||
} else if (data.type === 'disconnected') {
|
||||
connected.value = false
|
||||
addMessage('system', data.content)
|
||||
} else if (data.type === 'connecting') {
|
||||
addMessage('system', data.content)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 清理事件监听
|
||||
const cleanupEventListeners = () => {
|
||||
EventsOff('socket-message')
|
||||
EventsOff('socket-status')
|
||||
}
|
||||
|
||||
// 监听 messages 变化自动滚动
|
||||
watch(() => messages.value.length, () => {
|
||||
if (autoScroll.value) {
|
||||
nextTick(() => {
|
||||
scrollToBottom()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
setupEventListeners()
|
||||
nextTick(() => {
|
||||
if (messageListRef.value) {
|
||||
messageListHeight.value = messageListRef.value.clientHeight
|
||||
}
|
||||
})
|
||||
IsSocketConnected(protocol.value).then(res => {
|
||||
connected.value = res
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanupEventListeners()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="socket-debug">
|
||||
<div class="connection-section">
|
||||
<div class="connection-header">
|
||||
<div class="connection-title">
|
||||
<span>{{ t('socket.connection.title') }}</span>
|
||||
<el-tag v-if="connected" type="success" size="small">{{ t('socket.connection.status.connected') }}</el-tag>
|
||||
<el-tag v-else type="info" size="small">{{ t('socket.connection.status.disconnected') }}</el-tag>
|
||||
</div>
|
||||
<div class="connection-actions">
|
||||
<el-button
|
||||
v-if="!connected"
|
||||
type="primary"
|
||||
:loading="connecting"
|
||||
:icon="Connection"
|
||||
@click="connect"
|
||||
>
|
||||
{{ t('socket.connection.btn.connect') }}
|
||||
</el-button>
|
||||
<el-button v-else type="danger" :icon="SwitchButton" @click="disconnect">
|
||||
{{ t('socket.connection.btn.disconnect') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="connection-form">
|
||||
<el-form label-width="80px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6">
|
||||
<el-form-item :label="t('socket.connection.label.protocol')">
|
||||
<el-select v-model="protocol" style="width: 100%" :disabled="connected">
|
||||
<el-option
|
||||
v-for="p in protocols"
|
||||
:key="p.value"
|
||||
:label="p.label"
|
||||
:value="p.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item :label="t('socket.connection.label.address')">
|
||||
<el-input v-model="host" :placeholder="t('socket.connection.placeholder.host')" :disabled="connected" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="5">
|
||||
<el-form-item :label="t('socket.connection.label.port')">
|
||||
<el-input
|
||||
v-model="port"
|
||||
:placeholder="t('socket.connection.placeholder.port')"
|
||||
:disabled="connected"
|
||||
@input="handlePortInput"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="5">
|
||||
<el-form-item :label="t('socket.connection.label.path')">
|
||||
<el-input v-model="path" :placeholder="t('socket.connection.placeholder.path')" :disabled="connected" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item :label="t('socket.connection.label.headers')">
|
||||
<el-input v-model="headers" type="textarea" :rows="2" :placeholder="t('socket.connection.placeholder.headers')" :disabled="connected" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-section">
|
||||
<div class="message-header">
|
||||
<span>{{ t('socket.message.title') }} ({{ messages.length }})</span>
|
||||
<div class="message-actions">
|
||||
<el-checkbox v-model="autoScroll">{{ t('socket.message.auto_scroll') }}</el-checkbox>
|
||||
<el-button size="small" :icon="Delete" @click="clearMessages">{{ t('socket.message.btn.clear') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="messageListRef"
|
||||
class="message-list"
|
||||
>
|
||||
<div class="message-list-content">
|
||||
<div
|
||||
v-for="(msg, index) in messages"
|
||||
:key="index"
|
||||
:class="['message-item', msg.type]"
|
||||
>
|
||||
<div class="message-meta">
|
||||
<span class="message-time">{{ msg.time }}</span>
|
||||
<el-tag
|
||||
v-if="msg.type === 'sent'"
|
||||
type="primary"
|
||||
size="small"
|
||||
>{{ t('socket.message.tag.sent') }}</el-tag>
|
||||
<el-tag
|
||||
v-else-if="msg.type === 'received'"
|
||||
type="success"
|
||||
size="small"
|
||||
>{{ t('socket.message.tag.received') }}</el-tag>
|
||||
<el-tag
|
||||
v-else-if="msg.type === 'error'"
|
||||
type="danger"
|
||||
size="small"
|
||||
>{{ t('socket.message.tag.error') }}</el-tag>
|
||||
<el-tag
|
||||
v-else
|
||||
type="info"
|
||||
size="small"
|
||||
>{{ t('socket.message.tag.system') }}</el-tag>
|
||||
</div>
|
||||
<div class="message-content">{{ msg.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-if="messages.length === 0" :description="t('socket.message.empty')" />
|
||||
</div>
|
||||
|
||||
<div class="message-input">
|
||||
<el-input
|
||||
v-model="message"
|
||||
:placeholder="t('socket.message.placeholder')"
|
||||
:disabled="!connected"
|
||||
@keyup.enter="send"
|
||||
>
|
||||
<template #append>
|
||||
<el-button
|
||||
:icon="Promotion"
|
||||
:disabled="!connected || !message.trim()"
|
||||
@click="send"
|
||||
>
|
||||
{{ t('socket.message.btn.send') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.socket-debug {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.connection-section {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ebeef5;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.connection-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.connection-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.connection-form {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.message-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ebeef5;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.message-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
height: 360px;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.message-list::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.message-item.sent {
|
||||
background: #ecf5ff;
|
||||
border-left: 3px solid #409eff;
|
||||
}
|
||||
|
||||
.message-item.received {
|
||||
background: #f0f9eb;
|
||||
border-left: 3px solid #67c23a;
|
||||
}
|
||||
|
||||
.message-item.error {
|
||||
background: #fef0f0;
|
||||
border-left: 3px solid #f56c6c;
|
||||
}
|
||||
|
||||
.message-item.system {
|
||||
background: #f4f4f5;
|
||||
border-left: 3px solid #909399;
|
||||
}
|
||||
|
||||
.message-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
word-break: break-all;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
padding: 16px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,751 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import {
|
||||
Greet, Flashtime, Gettestjson,
|
||||
GetALLLang, GetCurrentLang, SetLanguage, GetLangTextMap,
|
||||
GetLogFiles, GetLogFileContent, SetLogLevel, GetLogLevel,
|
||||
GetSystemInfo, OpenFileSelect, OpenFolderSelect, SaveFileSelect,
|
||||
ReadFileContent, WriteFileContent, Notify
|
||||
} from '../../wailsjs/go/service/App'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { EventsOn } from '../../wailsjs/runtime'
|
||||
import { useI18n } from '../composables/useI18n'
|
||||
import {
|
||||
ChatDotRound, Timer, DataLine, Document,
|
||||
Setting, Monitor, View, Cpu, FolderOpened, Edit, Bell
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
const { t, textMap } = useI18n()
|
||||
|
||||
// ==================== 1. 前后端数据交互 (Greet) ====================
|
||||
const greetName = ref('')
|
||||
const greetResult = ref('')
|
||||
const greetLoading = ref(false)
|
||||
|
||||
function doGreet() {
|
||||
if (!greetName.value.trim()) {
|
||||
ElMessage.warning(t('input_name', '输入名字'))
|
||||
return
|
||||
}
|
||||
greetLoading.value = true
|
||||
Greet(greetName.value)
|
||||
.then((result) => {
|
||||
greetResult.value = result
|
||||
greetLoading.value = false
|
||||
})
|
||||
.catch((err) => {
|
||||
ElMessage.error(String(err))
|
||||
greetLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 2. 事件系统 (实时时间) ====================
|
||||
const systime = ref('Loading...')
|
||||
Flashtime()
|
||||
EventsOn('time', (time) => {
|
||||
systime.value = time
|
||||
})
|
||||
|
||||
// ==================== 3. JSON 数据交互 ====================
|
||||
const jsonData = ref(null)
|
||||
const dataLoading = ref(false)
|
||||
|
||||
function convertBytes(byteSize) {
|
||||
byteSize = Number(byteSize)
|
||||
if (byteSize < 0) return '0 B'
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let index = 0
|
||||
while (byteSize >= 1024 && index < units.length - 1) {
|
||||
byteSize /= 1024
|
||||
index++
|
||||
}
|
||||
return `${byteSize.toFixed(2)} ${units[index]}`
|
||||
}
|
||||
|
||||
function fetchJsonData() {
|
||||
dataLoading.value = true
|
||||
Gettestjson()
|
||||
.then((result) => {
|
||||
const parsed = JSON.parse(result)
|
||||
jsonData.value = parsed.data.map((item) => ({
|
||||
name: item.name,
|
||||
type: item.isdir ? t('folder', '文件夹') : t('file', '文件'),
|
||||
size: item.isdir ? '---' : convertBytes(item.size),
|
||||
time: '---',
|
||||
path: item.name,
|
||||
}))
|
||||
dataLoading.value = false
|
||||
})
|
||||
.catch((err) => {
|
||||
ElMessage.error(String(err))
|
||||
dataLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 4. 语言切换 ====================
|
||||
const langList = ref([])
|
||||
const currentLang = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const langInfos = await GetALLLang()
|
||||
langList.value = langInfos.map(info => ({
|
||||
code: info.language_code,
|
||||
name: info.language_name
|
||||
}))
|
||||
// 获取当前语言
|
||||
currentLang.value = await GetCurrentLang() || 'zh-CN'
|
||||
} catch (e) {
|
||||
console.error('初始化失败:', e)
|
||||
}
|
||||
})
|
||||
|
||||
async function handleLangChange(langCode) {
|
||||
try {
|
||||
await SetLanguage(langCode)
|
||||
// 重新加载语言映射,无需刷新页面
|
||||
const map = await GetLangTextMap()
|
||||
textMap.value = map || {}
|
||||
ElMessage.success(t('lang_switched', '语言已切换'))
|
||||
} catch (err) {
|
||||
ElMessage.error(String(err))
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 5. 日志等级设置 ====================
|
||||
const currentLogLevel = ref('INFO')
|
||||
const logLevelOptions = [
|
||||
{ value: 'DEBUG', label: 'DEBUG' },
|
||||
{ value: 'INFO', label: 'INFO' },
|
||||
{ value: 'WARN', label: 'WARN' },
|
||||
{ value: 'ERROR', label: 'ERROR' },
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const level = await GetLogLevel()
|
||||
currentLogLevel.value = level || 'INFO'
|
||||
} catch (e) {
|
||||
console.error('获取日志等级失败:', e)
|
||||
}
|
||||
})
|
||||
|
||||
async function handleLogLevelChange(level) {
|
||||
try {
|
||||
const ok = await SetLogLevel(level)
|
||||
if (ok) {
|
||||
ElMessage.success(t('log_level_changed', '日志等级已切换为') + ' ' + level)
|
||||
}
|
||||
} catch (err) {
|
||||
ElMessage.error(String(err))
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 6. 日志文件查看 ====================
|
||||
const logFiles = ref([])
|
||||
const selectedLogFile = ref('')
|
||||
const logContent = ref('')
|
||||
const logLoading = ref(false)
|
||||
|
||||
async function loadLogFiles() {
|
||||
try {
|
||||
logFiles.value = await GetLogFiles()
|
||||
} catch (err) {
|
||||
console.error('获取日志列表失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogFileChange(filename) {
|
||||
if (!filename) {
|
||||
logContent.value = ''
|
||||
return
|
||||
}
|
||||
logLoading.value = true
|
||||
try {
|
||||
logContent.value = await GetLogFileContent(filename)
|
||||
await nextTick()
|
||||
// 滚动到底部
|
||||
const el = document.getElementById('log-viewer')
|
||||
if (el) el.scrollTop = el.scrollHeight
|
||||
} catch (err) {
|
||||
ElMessage.error(String(err))
|
||||
} finally {
|
||||
logLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadLogFiles()
|
||||
})
|
||||
|
||||
// ==================== 7. 系统信息 ====================
|
||||
const sysInfo = ref(null)
|
||||
const sysInfoLoading = ref(false)
|
||||
|
||||
async function fetchSystemInfo() {
|
||||
sysInfoLoading.value = true
|
||||
try {
|
||||
sysInfo.value = await GetSystemInfo()
|
||||
} catch (err) {
|
||||
ElMessage.error(String(err))
|
||||
} finally {
|
||||
sysInfoLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => { fetchSystemInfo() })
|
||||
|
||||
// ==================== 8. 文件对话框 ====================
|
||||
const dialogFilePath = ref('')
|
||||
|
||||
async function handleOpenFile() {
|
||||
const path = await OpenFileSelect()
|
||||
if (path) {
|
||||
dialogFilePath.value = path
|
||||
}
|
||||
}
|
||||
|
||||
async function handleOpenFolder() {
|
||||
const path = await OpenFolderSelect()
|
||||
if (path) {
|
||||
dialogFilePath.value = path
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveFile() {
|
||||
const path = await SaveFileSelect()
|
||||
if (path) {
|
||||
dialogFilePath.value = path
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 9. 文件读写 ====================
|
||||
const rwFilePath = ref('')
|
||||
const rwFileContent = ref('')
|
||||
const rwLoading = ref(false)
|
||||
|
||||
async function handleReadFile() {
|
||||
if (!rwFilePath.value.trim()) {
|
||||
ElMessage.warning(t('file_path_empty', '请输入文件路径'))
|
||||
return
|
||||
}
|
||||
rwLoading.value = true
|
||||
try {
|
||||
const content = await ReadFileContent(rwFilePath.value)
|
||||
rwFileContent.value = content
|
||||
ElMessage.success(t('file_read_success', '文件读取成功'))
|
||||
} catch (err) {
|
||||
ElMessage.error(String(err))
|
||||
} finally {
|
||||
rwLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleWriteFile() {
|
||||
if (!rwFilePath.value.trim()) {
|
||||
ElMessage.warning(t('file_path_empty', '请输入文件路径'))
|
||||
return
|
||||
}
|
||||
rwLoading.value = true
|
||||
try {
|
||||
const ok = await WriteFileContent(rwFilePath.value, rwFileContent.value)
|
||||
if (ok) {
|
||||
ElMessage.success(t('file_write_success', '文件写入成功'))
|
||||
} else {
|
||||
ElMessage.error(t('file_write_failed', '文件写入失败'))
|
||||
}
|
||||
} catch (err) {
|
||||
ElMessage.error(String(err))
|
||||
} finally {
|
||||
rwLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 10. 事件通知 ====================
|
||||
const notifyTitle = ref('')
|
||||
const notifyMessage = ref('')
|
||||
const notifyLog = ref([])
|
||||
|
||||
EventsOn('notification', (data) => {
|
||||
notifyLog.value.unshift({
|
||||
time: new Date().toLocaleTimeString(),
|
||||
title: data.title,
|
||||
message: data.message,
|
||||
})
|
||||
ElMessage.info(`${t('notification_received', '收到通知')}: ${data.title}`)
|
||||
})
|
||||
|
||||
function handleSendNotify() {
|
||||
if (!notifyTitle.value.trim()) {
|
||||
ElMessage.warning(t('notify_title', '通知标题'))
|
||||
return
|
||||
}
|
||||
Notify(notifyTitle.value, notifyMessage.value)
|
||||
}
|
||||
|
||||
// ==================== 功能列表 ====================
|
||||
const features = [
|
||||
{ key: 'feature_i18n', desc: 'feature_i18n_desc', icon: '🌐', color: '#409EFF' },
|
||||
{ key: 'feature_backend', desc: 'feature_backend_desc', icon: '🔗', color: '#67C23A' },
|
||||
{ key: 'feature_event', desc: 'feature_event_desc', icon: '📡', color: '#E6A23C' },
|
||||
{ key: 'feature_window', desc: 'feature_window_desc', icon: '🪟', color: '#F56C6C' },
|
||||
{ key: 'feature_logger', desc: 'feature_logger_desc', icon: '📝', color: '#909399' },
|
||||
{ key: 'feature_json', desc: 'feature_json_desc', icon: '📦', color: '#9B59B6' },
|
||||
{ key: 'feature_ui', desc: 'feature_ui_desc', icon: '🎨', color: '#1ABC9C' },
|
||||
{ key: 'feature_embed', desc: 'feature_embed_desc', icon: '💾', color: '#3498DB' },
|
||||
{ key: 'feature_dialog', desc: 'feature_dialog_desc', icon: '📂', color: '#E74C3C' },
|
||||
{ key: 'feature_fileio', desc: 'feature_fileio_desc', icon: '✏️', color: '#F39C12' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="demo-container">
|
||||
<el-card class="section-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">{{ t('feature_overview', '功能概览') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<p class="overview-desc">{{ t('feature_overview_desc', '本模板集成了以下核心功能,可作为 Wails 项目的开发起点。') }}</p>
|
||||
<div class="feature-grid">
|
||||
<div v-for="feature in features" :key="feature.key" class="feature-item">
|
||||
<div class="feature-icon" :style="{ backgroundColor: feature.color + '15', color: feature.color }">
|
||||
{{ feature.icon }}
|
||||
</div>
|
||||
<div class="feature-info">
|
||||
<div class="feature-name">{{ t(feature.key, feature.key) }}</div>
|
||||
<div class="feature-desc">{{ t(feature.desc, feature.desc) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="section-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon><ChatDotRound /></el-icon>
|
||||
<span>{{ t('demo_data_interaction', '前后端数据交互') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<p class="section-desc">{{ t('demo_data_interaction_desc', '通过 Wails 绑定调用 Go 后端函数,获取返回值并展示。') }}</p>
|
||||
<div class="demo-row">
|
||||
<el-input
|
||||
v-model="greetName"
|
||||
:placeholder="t('greet_placeholder', '请输入你的名字')"
|
||||
style="max-width: 300px;"
|
||||
@keyup.enter="doGreet"
|
||||
/>
|
||||
<el-button @click="doGreet" type="primary" :loading="greetLoading">
|
||||
{{ t('send_greet', '发送问候') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-alert
|
||||
v-if="greetResult"
|
||||
:title="greetResult"
|
||||
type="success"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-top: 12px;"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<el-card class="section-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon><Timer /></el-icon>
|
||||
<span>{{ t('feature_event', '事件系统') }} - {{ t('realtime_time', '实时时间') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<p class="section-desc">{{ t('demo_event_desc', 'Go 后端通过 EventsEmit 推送数据,前端通过 EventsOn 监听接收。') }}</p>
|
||||
<div class="time-display">
|
||||
<span class="time-label">{{ t('time_label', '当前时间') }}</span>
|
||||
<span class="time-value">{{ systime }}</span>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="section-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon><DataLine /></el-icon>
|
||||
<span>{{ t('feature_json', 'JSON 数据处理') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<p class="section-desc">{{ t('demo_json_desc', '后端返回 JSON 字符串,前端解析后以表格形式展示。') }}</p>
|
||||
<div class="demo-row" style="margin-bottom: 16px;">
|
||||
<el-button @click="fetchJsonData" type="primary" :loading="dataLoading">
|
||||
{{ t('fetch_data', '获取数据') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table v-if="jsonData" :data="jsonData" stripe border style="width: 100%" max-height="300">
|
||||
<el-table-column prop="name" :label="t('file_name', '文件名')" min-width="150" />
|
||||
<el-table-column prop="type" :label="t('file_type', '文件类型')" min-width="100" />
|
||||
<el-table-column prop="size" :label="t('file_size', '文件大小')" min-width="100" />
|
||||
<el-table-column prop="time" :label="t('create_time', '创建时间')" min-width="160" />
|
||||
</el-table>
|
||||
<el-empty v-else :description="t('click_to_fetch', '点击按钮获取数据')" />
|
||||
</el-card>
|
||||
|
||||
<el-card class="section-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon><Monitor /></el-icon>
|
||||
<span>{{ t('demo_lang_switch', '语言切换 (i18n)') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<p class="section-desc">{{ t('demo_lang_switch_desc', '切换应用语言,支持嵌入语言包和本地语言文件。') }}</p>
|
||||
<div class="demo-row">
|
||||
<span class="demo-label">{{ t('current_lang', '当前语言') }}:</span>
|
||||
<el-select v-model="currentLang" style="width: 200px;" @change="handleLangChange">
|
||||
<el-option
|
||||
v-for="lang in langList"
|
||||
:key="lang.code"
|
||||
:label="lang.name"
|
||||
:value="lang.code"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="section-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon><Setting /></el-icon>
|
||||
<span>{{ t('demo_log_level', '日志等级设置') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<p class="section-desc">{{ t('demo_log_level_desc', '动态调整日志输出等级。') }}</p>
|
||||
<div class="demo-row">
|
||||
<span class="demo-label">{{ t('log_level', '日志等级') }}:</span>
|
||||
<el-select v-model="currentLogLevel" style="width: 200px;" @change="handleLogLevelChange">
|
||||
<el-option
|
||||
v-for="opt in logLevelOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="section-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ t('demo_log_viewer', '日志文件查看') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<p class="section-desc">{{ t('demo_log_viewer_desc', '获取日志文件列表并查看内容,展示前后端数据交互的实际应用。') }}</p>
|
||||
<div class="demo-row" style="margin-bottom: 12px;">
|
||||
<el-select
|
||||
v-model="selectedLogFile"
|
||||
:placeholder="t('select_log_file', '选择日志文件')"
|
||||
style="width: 300px;"
|
||||
@change="handleLogFileChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="file in logFiles"
|
||||
:key="file"
|
||||
:label="file"
|
||||
:value="file"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button @click="loadLogFiles" :icon="View" circle />
|
||||
</div>
|
||||
<div
|
||||
v-if="logContent"
|
||||
id="log-viewer"
|
||||
class="log-viewer"
|
||||
v-loading="logLoading"
|
||||
>
|
||||
<pre class="log-pre">{{ logContent }}</pre>
|
||||
</div>
|
||||
<el-empty v-else :description="t('select_log_to_view', '选择一个日志文件查看内容')" />
|
||||
</el-card>
|
||||
|
||||
<el-card class="section-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon><Cpu /></el-icon>
|
||||
<span>{{ t('demo_system_info', '系统信息') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<p class="section-desc">{{ t('demo_system_info_desc', '获取当前操作系统、架构、CPU 核心数等系统信息。') }}</p>
|
||||
<div class="demo-row" style="margin-bottom: 12px;">
|
||||
<el-button @click="fetchSystemInfo" type="primary" :loading="sysInfoLoading">
|
||||
{{ t('fetch_data', '获取数据') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-descriptions v-if="sysInfo" :column="2" border>
|
||||
<el-descriptions-item :label="t('os', '操作系统')">{{ sysInfo.os }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('arch', '架构')">{{ sysInfo.arch }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('cpu_count', 'CPU 核心数')">{{ sysInfo.num_cpu }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('hostname', '主机名')">{{ sysInfo.hostname }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('go_version', 'Go 版本')">{{ sysInfo.go_ver }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('time_label', '当前时间')">{{ sysInfo.time }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('process_name', '进程名')">{{ sysInfo.process_name }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-empty v-else :description="t('click_to_fetch', '点击按钮获取数据')" />
|
||||
</el-card>
|
||||
|
||||
<el-card class="section-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon><FolderOpened /></el-icon>
|
||||
<span>{{ t('demo_file_dialog', '文件对话框') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<p class="section-desc">{{ t('demo_file_dialog_desc', '调用系统原生文件对话框,支持打开文件、选择目录和保存文件。') }}</p>
|
||||
<div class="demo-row" style="margin-bottom: 12px;">
|
||||
<el-button @click="handleOpenFile" type="primary">{{ t('open_file', '打开文件') }}</el-button>
|
||||
<el-button @click="handleOpenFolder" type="success">{{ t('open_folder', '选择目录') }}</el-button>
|
||||
<el-button @click="handleSaveFile" type="warning">{{ t('save_file', '保存文件') }}</el-button>
|
||||
</div>
|
||||
<el-alert
|
||||
v-if="dialogFilePath"
|
||||
:title="t('selected_path', '已选择路径') + ': ' + dialogFilePath"
|
||||
type="info"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
<el-empty v-else :description="t('click_to_fetch', '点击按钮获取数据')" />
|
||||
</el-card>
|
||||
|
||||
<el-card class="section-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon><Edit /></el-icon>
|
||||
<span>{{ t('demo_file_read_write', '文件读写') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<p class="section-desc">{{ t('demo_file_read_write_desc', '读取和写入本地文件,展示文件系统交互能力。') }}</p>
|
||||
<div class="demo-row" style="margin-bottom: 12px;">
|
||||
<el-input
|
||||
v-model="rwFilePath"
|
||||
:placeholder="t('file_path', '文件路径')"
|
||||
style="max-width: 400px;"
|
||||
/>
|
||||
<el-button @click="handleReadFile" type="primary" :loading="rwLoading">
|
||||
{{ t('read_file', '读取文件') }}
|
||||
</el-button>
|
||||
<el-button @click="handleWriteFile" type="success" :loading="rwLoading">
|
||||
{{ t('write_file', '写入文件') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="rwFileContent"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
:placeholder="t('file_content', '文件内容')"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<el-card class="section-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<el-icon><Bell /></el-icon>
|
||||
<span>{{ t('demo_notification', '事件通知') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<p class="section-desc">{{ t('demo_notification_desc', '后端通过 EventsEmit 向前端发送自定义通知事件。') }}</p>
|
||||
<div class="demo-row" style="margin-bottom: 12px;">
|
||||
<el-input
|
||||
v-model="notifyTitle"
|
||||
:placeholder="t('notify_title', '通知标题')"
|
||||
style="max-width: 200px;"
|
||||
/>
|
||||
<el-input
|
||||
v-model="notifyMessage"
|
||||
:placeholder="t('notify_message', '通知内容')"
|
||||
style="max-width: 300px;"
|
||||
/>
|
||||
<el-button @click="handleSendNotify" type="primary">
|
||||
{{ t('send_notify', '发送通知') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="notifyLog.length" class="notify-log">
|
||||
<div v-for="(item, idx) in notifyLog" :key="idx" class="notify-item">
|
||||
<span class="notify-time">[{{ item.time }}]</span>
|
||||
<span class="notify-title">{{ item.title }}</span>
|
||||
<span class="notify-msg" v-if="item.message"> - {{ item.message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else :description="t('click_to_fetch', '点击按钮获取数据')" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.section-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.overview-desc,
|
||||
.section-desc {
|
||||
color: #666;
|
||||
margin-bottom: 16px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #f0f0f0;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.feature-item:hover {
|
||||
border-color: #409EFF;
|
||||
box-shadow: 0 2px 12px rgba(64, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.feature-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.feature-name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.demo-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.demo-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.time-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.time-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.time-value {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.log-viewer {
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.log-pre {
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
font-family: 'Cascadia Code', 'Fira Code', 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: #d4d4d4;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.notify-log {
|
||||
background: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.notify-item {
|
||||
padding: 6px 0;
|
||||
font-size: 13px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.notify-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.notify-time {
|
||||
color: #909399;
|
||||
margin-right: 8px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.notify-title {
|
||||
font-weight: 600;
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.notify-msg {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.feature-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,771 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from '../composables/useI18n'
|
||||
import {
|
||||
SendHttpRequest,
|
||||
OpenFileSelect,
|
||||
GetRequestLogs,
|
||||
DeleteRequestLog,
|
||||
ClearRequestLogs,
|
||||
SearchRequestLogs
|
||||
} from '../../wailsjs/go/service/App'
|
||||
import { Delete, Upload, Notebook, Search } from '@element-plus/icons-vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 请求相关状态
|
||||
const url = ref('')
|
||||
const response = ref({})
|
||||
const loading = ref(false)
|
||||
const method = ref('GET')
|
||||
const headers = ref([{ key: '', value: '' }])
|
||||
const cookies = ref('')
|
||||
const bodyType = ref('text')
|
||||
const body = ref('')
|
||||
const filePath = ref('')
|
||||
const contentType = ref('')
|
||||
const proxy = ref('')
|
||||
const timeout = ref(30)
|
||||
const insecure = ref(false)
|
||||
const saveLog = ref(true)
|
||||
|
||||
// 响应展示相关
|
||||
const responseTab = ref('body')
|
||||
|
||||
// 日志相关状态
|
||||
const logs = ref([])
|
||||
const selectedLog = ref(null)
|
||||
const logSearchKeyword = ref('')
|
||||
const showLogDialog = ref(false)
|
||||
|
||||
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']
|
||||
|
||||
// 获取方法颜色
|
||||
const getMethodColor = (method) => {
|
||||
const colors = {
|
||||
'GET': '#67c23a',
|
||||
'POST': '#409eff',
|
||||
'PUT': '#e6a23c',
|
||||
'DELETE': '#f56c6c',
|
||||
'PATCH': '#909399',
|
||||
'HEAD': '#909399',
|
||||
'OPTIONS': '#909399'
|
||||
}
|
||||
return colors[method] || '#909399'
|
||||
}
|
||||
|
||||
// 获取状态码类型
|
||||
const getStatusType = (code) => {
|
||||
if (code >= 200 && code < 300) return 'success'
|
||||
if (code >= 300 && code < 400) return 'warning'
|
||||
if (code >= 400) return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
// 检查是否是 JSON
|
||||
const isJson = computed(() => {
|
||||
if (!response.value || !response.value.body) return false
|
||||
try {
|
||||
JSON.parse(response.value.body)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
// 检查是否有响应数据
|
||||
const hasResponse = computed(() => {
|
||||
return response.value && response.value.statusCode
|
||||
})
|
||||
|
||||
// 格式化响应头为文本
|
||||
const formatResponseHeaders = computed(() => {
|
||||
if (!response.value || !response.value.headers) return ''
|
||||
return Object.entries(response.value.headers)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join('\n')
|
||||
})
|
||||
|
||||
// 格式化内容大小
|
||||
const formatContentSize = computed(() => {
|
||||
if (!response.value) return '-'
|
||||
const size = response.value.size || response.value.contentLength
|
||||
if (!size || size < 0) return '-'
|
||||
if (size < 1024) return `${size} B`
|
||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB`
|
||||
return `${(size / (1024 * 1024)).toFixed(2)} MB`
|
||||
})
|
||||
|
||||
// 添加请求头
|
||||
const addHeader = () => {
|
||||
headers.value.push({ key: '', value: '' })
|
||||
}
|
||||
|
||||
// 删除请求头
|
||||
const removeHeader = (index) => {
|
||||
headers.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 格式化请求头
|
||||
const formatHeaders = computed(() => {
|
||||
return headers.value
|
||||
.filter(h => h.key && h.value)
|
||||
.map(h => `${h.key}: ${h.value}`)
|
||||
.join('\n')
|
||||
})
|
||||
|
||||
// 选择文件
|
||||
const selectFile = async () => {
|
||||
const path = await OpenFileSelect()
|
||||
if (path) {
|
||||
filePath.value = path
|
||||
bodyType.value = 'file'
|
||||
}
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
const sendRequest = async () => {
|
||||
if (!url.value) {
|
||||
ElMessage.warning(t('webdebug.msg.url_required'))
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
response.value = {}
|
||||
|
||||
try {
|
||||
const result = await SendHttpRequest(
|
||||
method.value,
|
||||
url.value,
|
||||
formatHeaders.value,
|
||||
cookies.value,
|
||||
proxy.value,
|
||||
bodyType.value,
|
||||
body.value,
|
||||
filePath.value,
|
||||
contentType.value,
|
||||
timeout.value,
|
||||
insecure.value,
|
||||
saveLog.value
|
||||
)
|
||||
response.value = result || {}
|
||||
responseTab.value = 'body'
|
||||
} catch (e) {
|
||||
ElMessage.error(t('webdebug.msg.request_failed') + e)
|
||||
response.value = { error: e.toString() }
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载日志列表
|
||||
const loadLogs = async () => {
|
||||
try {
|
||||
if (logSearchKeyword.value) {
|
||||
logs.value = await SearchRequestLogs(logSearchKeyword.value) || []
|
||||
} else {
|
||||
logs.value = await GetRequestLogs() || []
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(t('webdebug.msg.load_logs_failed') + e)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开日志窗口
|
||||
const openLogDialog = async () => {
|
||||
await loadLogs()
|
||||
showLogDialog.value = true
|
||||
}
|
||||
|
||||
// 选择日志
|
||||
const selectLog = (log) => {
|
||||
selectedLog.value = log
|
||||
}
|
||||
|
||||
// 双击日志 - 添加到调试页面
|
||||
const applyLog = (log) => {
|
||||
method.value = log.method
|
||||
url.value = log.url
|
||||
|
||||
// 恢复请求头
|
||||
if (log.headers) {
|
||||
headers.value = Object.entries(log.headers).map(([key, value]) => ({ key, value }))
|
||||
} else {
|
||||
headers.value = [{ key: '', value: '' }]
|
||||
}
|
||||
|
||||
// 恢复 Cookie
|
||||
if (log.cookies) {
|
||||
cookies.value = Object.entries(log.cookies).map(([key, value]) => `${key}=${value}`).join('; ')
|
||||
}
|
||||
|
||||
// 恢复请求体
|
||||
if (log.body) {
|
||||
body.value = log.body
|
||||
bodyType.value = log.bodyType || 'text'
|
||||
}
|
||||
|
||||
showLogDialog.value = false
|
||||
}
|
||||
|
||||
// 删除日志
|
||||
const deleteLog = async (id) => {
|
||||
try {
|
||||
await DeleteRequestLog(id)
|
||||
await loadLogs()
|
||||
if (selectedLog.value && selectedLog.value.id === id) {
|
||||
selectedLog.value = null
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(t('webdebug.msg.delete_failed') + e)
|
||||
}
|
||||
}
|
||||
|
||||
// 清空日志
|
||||
const clearLogs = async () => {
|
||||
try {
|
||||
await ClearRequestLogs()
|
||||
logs.value = []
|
||||
selectedLog.value = null
|
||||
} catch (e) {
|
||||
ElMessage.error(t('webdebug.msg.clear_failed') + e)
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索日志
|
||||
const searchLogs = async () => {
|
||||
await loadLogs()
|
||||
}
|
||||
|
||||
// 复制到 JSON 解析器
|
||||
const copyToJsonParser = () => {
|
||||
if (response.value && response.value.body) {
|
||||
const event = new CustomEvent('copy-to-json', { detail: response.value.body })
|
||||
window.dispatchEvent(event)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="web-debug">
|
||||
<div class="request-section">
|
||||
<div class="url-bar">
|
||||
<el-select v-model="method" class="method-select">
|
||||
<el-option v-for="m in methods" :key="m" :label="m" :value="m" />
|
||||
</el-select>
|
||||
<el-input v-model="url" :placeholder="t('webdebug.url_placeholder')" class="url-input" />
|
||||
<el-button type="primary" :loading="loading" @click="sendRequest">
|
||||
{{ t('webdebug.btn_send') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-tabs type="border-card" class="request-tabs">
|
||||
<el-tab-pane :label="t('webdebug.tab.headers')">
|
||||
<div class="headers-section">
|
||||
<div v-for="(header, index) in headers" :key="index" class="header-row">
|
||||
<el-input v-model="header.key" :placeholder="t('webdebug.header.key_placeholder')" />
|
||||
<el-input v-model="header.value" :placeholder="t('webdebug.header.value_placeholder')" />
|
||||
<el-button type="danger" :icon="Delete" circle size="small" @click="removeHeader(index)" />
|
||||
</div>
|
||||
<el-button type="primary" plain @click="addHeader">{{ t('webdebug.header.add') }}</el-button>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('webdebug.tab.body')">
|
||||
<div class="body-section">
|
||||
<el-radio-group v-model="bodyType" class="body-type-group">
|
||||
<el-radio-button label="text">{{ t('webdebug.body.type_text') }}</el-radio-button>
|
||||
<el-radio-button label="json">{{ t('webdebug.body.type_json') }}</el-radio-button>
|
||||
<el-radio-button label="form">{{ t('webdebug.body.type_form') }}</el-radio-button>
|
||||
<el-radio-button label="file">{{ t('webdebug.body.type_file') }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
|
||||
<template v-if="bodyType === 'file'">
|
||||
<div class="file-section">
|
||||
<el-input v-model="filePath" :placeholder="t('webdebug.body.file_placeholder')" readonly>
|
||||
<template #append>
|
||||
<el-button :icon="Upload" @click="selectFile">{{ t('webdebug.body.file_btn') }}</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-input v-model="body" type="textarea" :rows="8" :placeholder="bodyType === 'json' ? t('webdebug.body.json_placeholder') : t('webdebug.body.text_placeholder')" />
|
||||
</template>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('webdebug.tab.cookie')">
|
||||
<el-input v-model="cookies" type="textarea" :rows="5" :placeholder="t('webdebug.body.cookie_placeholder')" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('webdebug.tab.advanced')">
|
||||
<div class="advanced-settings">
|
||||
<el-form label-width="120px">
|
||||
<el-form-item :label="t('webdebug.advanced.proxy_label')">
|
||||
<el-input v-model="proxy" :placeholder="t('webdebug.advanced.proxy_placeholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('webdebug.advanced.timeout_label')">
|
||||
<el-input-number v-model="timeout" :min="1" :max="300" /> {{ t('webdebug.advanced.timeout_unit') }}
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('webdebug.advanced.content_type_label')">
|
||||
<el-input v-model="contentType" :placeholder="t('webdebug.advanced.content_type_placeholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('webdebug.advanced.insecure_label')">
|
||||
<el-switch v-model="insecure" />
|
||||
</el-form-item>
|
||||
<el-divider content-position="left">{{ t('webdebug.advanced.log_divider') }}</el-divider>
|
||||
<el-form-item :label="t('webdebug.advanced.save_log_label')">
|
||||
<el-switch v-model="saveLog" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('webdebug.advanced.log_manage_label')">
|
||||
<el-button type="primary" :icon="Notebook" @click="openLogDialog">
|
||||
{{ t('webdebug.advanced.view_logs_btn') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<div class="response-section">
|
||||
<div class="response-header">
|
||||
<div class="response-title">
|
||||
<span>{{ t('webdebug.response.title') }}</span>
|
||||
<div class="response-info" v-if="hasResponse">
|
||||
<el-tag v-if="response.statusCode" :type="getStatusType(response.statusCode)">
|
||||
{{ response.statusCode }} {{ response.status }}
|
||||
</el-tag>
|
||||
<el-tag v-if="response.time" type="info">
|
||||
{{ response.time }}
|
||||
</el-tag>
|
||||
<el-tag v-if="response.size" type="info">
|
||||
{{ (response.size / 1024).toFixed(2) }} KB
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<el-radio-group v-model="responseTab" size="small">
|
||||
<el-radio-button label="body">{{ t('webdebug.response.tab.body') }}</el-radio-button>
|
||||
<el-radio-button label="headers">{{ t('webdebug.response.tab.headers') }}</el-radio-button>
|
||||
<el-radio-button label="more">{{ t('webdebug.response.tab.more') }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="response-content">
|
||||
<div v-if="responseTab === 'body'" class="response-body-container">
|
||||
<el-input
|
||||
:value="response.body || ''"
|
||||
type="textarea"
|
||||
:rows="18"
|
||||
readonly
|
||||
:placeholder="t('webdebug.response.body_placeholder')"
|
||||
/>
|
||||
<div class="response-actions" v-if="hasResponse && response.body && isJson">
|
||||
<el-button size="small" @click="copyToJsonParser">
|
||||
{{ t('webdebug.response.send_to_json') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="responseTab === 'headers'" class="response-headers-container">
|
||||
<el-input
|
||||
:value="formatResponseHeaders"
|
||||
type="textarea"
|
||||
:rows="18"
|
||||
readonly
|
||||
:placeholder="t('webdebug.response.headers_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="responseTab === 'more'" class="more-info">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item :label="t('webdebug.response.status_code_label')">{{ response.statusCode || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('webdebug.response.status_label')">{{ response.status || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('webdebug.response.content_type_label')">{{ response.contentType || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('webdebug.response.content_length_label')">{{ formatContentSize }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('webdebug.response.request_time_label')">{{ response.time || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="showLogDialog"
|
||||
:title="t('webdebug.log.title')"
|
||||
width="90%"
|
||||
:style="{ maxWidth: '900px' }"
|
||||
top="5vh"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="log-dialog-content">
|
||||
<div class="log-list-panel">
|
||||
<div class="log-list-header">
|
||||
<el-input
|
||||
v-model="logSearchKeyword"
|
||||
:placeholder="t('webdebug.log.search_placeholder')"
|
||||
clearable
|
||||
@keyup.enter="searchLogs"
|
||||
>
|
||||
<template #append>
|
||||
<el-button :icon="Search" @click="searchLogs" />
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button type="danger" size="small" @click="clearLogs">{{ t('webdebug.log.clear') }}</el-button>
|
||||
</div>
|
||||
|
||||
<div class="log-list">
|
||||
<div
|
||||
v-for="log in logs"
|
||||
:key="log.id"
|
||||
:class="['log-item', { selected: selectedLog && selectedLog.id === log.id }]"
|
||||
@click="selectLog(log)"
|
||||
@dblclick="applyLog(log)"
|
||||
>
|
||||
<div class="log-item-header">
|
||||
<span class="log-id">#{{ log.id }}</span>
|
||||
<el-tag :color="getMethodColor(log.method)" effect="dark" size="small">
|
||||
{{ log.method }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="log-item-info">
|
||||
<div class="log-host">{{ log.host }}</div>
|
||||
<div class="log-path">{{ log.path }}</div>
|
||||
</div>
|
||||
<div class="log-item-meta">
|
||||
<el-tag :type="getStatusType(log.statusCode)" size="small">
|
||||
{{ log.statusCode }}
|
||||
</el-tag>
|
||||
<span class="log-time">{{ log.createdAt }}</span>
|
||||
</div>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
:icon="Delete"
|
||||
circle
|
||||
class="log-delete-btn"
|
||||
@click.stop="deleteLog(log.id)"
|
||||
/>
|
||||
</div>
|
||||
<el-empty v-if="logs.length === 0" :description="t('webdebug.log.empty')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="log-detail-panel" v-if="selectedLog">
|
||||
<el-tabs type="border-card">
|
||||
<el-tab-pane :label="t('webdebug.log.tab.request_info')">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item :label="t('webdebug.log.request.method_label')">{{ selectedLog.method }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('webdebug.log.request.url_label')">{{ selectedLog.url }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('webdebug.log.request.host_label')">{{ selectedLog.host }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('webdebug.log.request.path_label')">{{ selectedLog.path }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('webdebug.log.request.body_label')" v-if="selectedLog.body">
|
||||
<el-input :value="selectedLog.body" type="textarea" :rows="5" readonly />
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('webdebug.log.tab.response_info')">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item :label="t('webdebug.log.response.status_code_label')">{{ selectedLog.statusCode }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('webdebug.log.response.status_label')">{{ selectedLog.status }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('webdebug.log.response.size_label')">{{ selectedLog.respSize ? (selectedLog.respSize / 1024).toFixed(2) + ' KB' : '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('webdebug.log.response.time_label')">{{ selectedLog.time }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="log-resp-body">
|
||||
<div class="log-resp-header">{{ t('webdebug.log.response.body_label') }}</div>
|
||||
<el-input :value="selectedLog.respBody" type="textarea" :rows="10" readonly />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<div class="log-detail-actions">
|
||||
<el-button type="primary" @click="applyLog(selectedLog)">{{ t('webdebug.log.load_to_debug') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-detail-panel empty" v-else>
|
||||
<el-empty :description="t('webdebug.log.select_hint')" />
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.web-debug {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.request-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.url-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.method-select {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.url-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.request-tabs {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.request-tabs :deep(.el-tabs) {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.request-tabs :deep(.el-tabs__header) {
|
||||
margin: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.request-tabs :deep(.el-tabs__nav-wrap) {
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.request-tabs :deep(.el-tabs__item) {
|
||||
padding: 0 24px;
|
||||
height: 42px;
|
||||
line-height: 42px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.request-tabs :deep(.el-tabs__content) {
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.request-tabs :deep(.el-tab-pane) {
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.headers-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-row .el-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.body-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.body-type-group {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.file-section {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.advanced-settings {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.response-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.response-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.response-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.response-info {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.response-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.response-body-container,
|
||||
.response-headers-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.response-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.more-info {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
/* 日志对话框样式 */
|
||||
.log-dialog-content {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
height: 50vh;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.log-list-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.log-list-header {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.log-list {
|
||||
flex: 1;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.log-list::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.log-item:hover {
|
||||
border-color: #409eff;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.log-item.selected {
|
||||
border-color: #409eff;
|
||||
background-color: #ecf5ff;
|
||||
}
|
||||
|
||||
.log-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.log-id {
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.log-item-info {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.log-host {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.log-path {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.log-item-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.log-delete-btn {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.log-item:hover .log-delete-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.log-detail-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.log-detail-panel.empty {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.log-resp-body {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.log-resp-header {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.log-detail-actions {
|
||||
padding: 12px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { GetLangTextMap } from '../../wailsjs/go/service/App'
|
||||
|
||||
// 全局单例状态
|
||||
const textMap = ref({})
|
||||
const isLoaded = ref(false)
|
||||
|
||||
export function useI18n() {
|
||||
const loadTextMap = async () => {
|
||||
try {
|
||||
const map = await GetLangTextMap()
|
||||
textMap.value = map || {}
|
||||
isLoaded.value = true
|
||||
} catch (error) {
|
||||
console.error('加载语言包失败:', error)
|
||||
textMap.value = {}
|
||||
isLoaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const t = (key, defaultValue = '') => {
|
||||
return textMap.value[key] || defaultValue || key
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!isLoaded.value) {
|
||||
loadTextMap()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
t,
|
||||
textMap,
|
||||
isLoaded,
|
||||
loadTextMap
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createApp } from 'vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import App from './App.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(ElementPlus)
|
||||
app.mount('#app')
|
||||
|
||||
document.body.style.overflow = 'hidden';
|
||||
@@ -0,0 +1,31 @@
|
||||
/* 基础样式 */
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* 隐藏滚动条 - 通用方法 */
|
||||
.hide-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Element Plus textarea 内部滚动条隐藏 */
|
||||
.el-textarea__inner {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.el-textarea__inner::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import {defineConfig} from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
AutoImport({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
Components({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
export namespace api {
|
||||
|
||||
export class CookieInfo {
|
||||
name: string;
|
||||
value: string;
|
||||
path: string;
|
||||
domain: string;
|
||||
expires: string;
|
||||
maxAge: number;
|
||||
secure: boolean;
|
||||
httpOnly: boolean;
|
||||
sameSite: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new CookieInfo(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.name = source["name"];
|
||||
this.value = source["value"];
|
||||
this.path = source["path"];
|
||||
this.domain = source["domain"];
|
||||
this.expires = source["expires"];
|
||||
this.maxAge = source["maxAge"];
|
||||
this.secure = source["secure"];
|
||||
this.httpOnly = source["httpOnly"];
|
||||
this.sameSite = source["sameSite"];
|
||||
}
|
||||
}
|
||||
export class HttpResponse {
|
||||
statusCode: number;
|
||||
status: string;
|
||||
headers: Record<string, string>;
|
||||
cookies: CookieInfo[];
|
||||
body: string;
|
||||
contentType: string;
|
||||
contentLength: number;
|
||||
time: string;
|
||||
size: number;
|
||||
error: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new HttpResponse(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.statusCode = source["statusCode"];
|
||||
this.status = source["status"];
|
||||
this.headers = source["headers"];
|
||||
this.cookies = this.convertValues(source["cookies"], CookieInfo);
|
||||
this.body = source["body"];
|
||||
this.contentType = source["contentType"];
|
||||
this.contentLength = source["contentLength"];
|
||||
this.time = source["time"];
|
||||
this.size = source["size"];
|
||||
this.error = source["error"];
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class JsExecResult {
|
||||
output: string;
|
||||
result: string;
|
||||
error: string;
|
||||
duration: string;
|
||||
success: boolean;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new JsExecResult(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.output = source["output"];
|
||||
this.result = source["result"];
|
||||
this.error = source["error"];
|
||||
this.duration = source["duration"];
|
||||
this.success = source["success"];
|
||||
}
|
||||
}
|
||||
export class RequestLog {
|
||||
id: number;
|
||||
method: string;
|
||||
url: string;
|
||||
host: string;
|
||||
path: string;
|
||||
headers: Record<string, string>;
|
||||
cookies: Record<string, string>;
|
||||
bodyType: string;
|
||||
body: string;
|
||||
statusCode: number;
|
||||
status: string;
|
||||
respHeaders: Record<string, string>;
|
||||
respBody: string;
|
||||
respSize: number;
|
||||
time: string;
|
||||
createdAt: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new RequestLog(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.id = source["id"];
|
||||
this.method = source["method"];
|
||||
this.url = source["url"];
|
||||
this.host = source["host"];
|
||||
this.path = source["path"];
|
||||
this.headers = source["headers"];
|
||||
this.cookies = source["cookies"];
|
||||
this.bodyType = source["bodyType"];
|
||||
this.body = source["body"];
|
||||
this.statusCode = source["statusCode"];
|
||||
this.status = source["status"];
|
||||
this.respHeaders = source["respHeaders"];
|
||||
this.respBody = source["respBody"];
|
||||
this.respSize = source["respSize"];
|
||||
this.time = source["time"];
|
||||
this.createdAt = source["createdAt"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace global {
|
||||
|
||||
export class LanguageInfo {
|
||||
language_name: string;
|
||||
language_code: string;
|
||||
textmap_path: string;
|
||||
translation_progress: string;
|
||||
translator: string;
|
||||
last_updated: string;
|
||||
version: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new LanguageInfo(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.language_name = source["language_name"];
|
||||
this.language_code = source["language_code"];
|
||||
this.textmap_path = source["textmap_path"];
|
||||
this.translation_progress = source["translation_progress"];
|
||||
this.translator = source["translator"];
|
||||
this.last_updated = source["last_updated"];
|
||||
this.version = source["version"];
|
||||
}
|
||||
}
|
||||
export class LanguagePack {
|
||||
language_name: string;
|
||||
language_code: string;
|
||||
textmap_path: string;
|
||||
translation_progress: string;
|
||||
translator: string;
|
||||
last_updated: string;
|
||||
version: string;
|
||||
textmap: Record<string, string>;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new LanguagePack(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.language_name = source["language_name"];
|
||||
this.language_code = source["language_code"];
|
||||
this.textmap_path = source["textmap_path"];
|
||||
this.translation_progress = source["translation_progress"];
|
||||
this.translator = source["translator"];
|
||||
this.last_updated = source["last_updated"];
|
||||
this.version = source["version"];
|
||||
this.textmap = source["textmap"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace service {
|
||||
|
||||
export class SystemInfo {
|
||||
os: string;
|
||||
arch: string;
|
||||
num_cpu: number;
|
||||
hostname: string;
|
||||
go_ver: string;
|
||||
time: string;
|
||||
process_name: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new SystemInfo(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.os = source["os"];
|
||||
this.arch = source["arch"];
|
||||
this.num_cpu = source["num_cpu"];
|
||||
this.hostname = source["hostname"];
|
||||
this.go_ver = source["go_ver"];
|
||||
this.time = source["time"];
|
||||
this.process_name = source["process_name"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Vendored
+73
@@ -0,0 +1,73 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {api} from '../models';
|
||||
import {global} from '../models';
|
||||
import {service} from '../models';
|
||||
|
||||
export function ClearRequestLogs():Promise<boolean>;
|
||||
|
||||
export function ConnectSocket(arg1:string,arg2:string,arg3:number,arg4:string,arg5:string):Promise<boolean>;
|
||||
|
||||
export function DeleteRequestLog(arg1:number):Promise<boolean>;
|
||||
|
||||
export function DisconnectSocket(arg1:string):Promise<boolean>;
|
||||
|
||||
export function ExecuteJs(arg1:string):Promise<api.JsExecResult>;
|
||||
|
||||
export function Flashtime():Promise<void>;
|
||||
|
||||
export function GetALLLang():Promise<Array<global.LanguageInfo>>;
|
||||
|
||||
export function GetCurrentLang():Promise<string>;
|
||||
|
||||
export function GetLangPack():Promise<global.LanguagePack>;
|
||||
|
||||
export function GetLangTextMap():Promise<Record<string, string>>;
|
||||
|
||||
export function GetLogFileContent(arg1:string):Promise<string>;
|
||||
|
||||
export function GetLogFiles():Promise<Array<string>>;
|
||||
|
||||
export function GetLogLevel():Promise<string>;
|
||||
|
||||
export function GetProcessName():Promise<string>;
|
||||
|
||||
export function GetRequestLog(arg1:number):Promise<api.RequestLog>;
|
||||
|
||||
export function GetRequestLogs():Promise<Array<api.RequestLog>>;
|
||||
|
||||
export function GetSystemInfo():Promise<service.SystemInfo>;
|
||||
|
||||
export function Gettestjson():Promise<string>;
|
||||
|
||||
export function Greet(arg1:string):Promise<string>;
|
||||
|
||||
export function IsSocketConnected(arg1:string):Promise<boolean>;
|
||||
|
||||
export function Notify(arg1:string,arg2:string):Promise<void>;
|
||||
|
||||
export function OpenFileSelect():Promise<string>;
|
||||
|
||||
export function OpenFolderSelect():Promise<string>;
|
||||
|
||||
export function ReadFileContent(arg1:string):Promise<string>;
|
||||
|
||||
export function SaveFileSelect():Promise<string>;
|
||||
|
||||
export function SearchRequestLogs(arg1:string):Promise<Array<api.RequestLog>>;
|
||||
|
||||
export function SendHttpRequest(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string,arg6:string,arg7:string,arg8:string,arg9:string,arg10:number,arg11:boolean,arg12:boolean):Promise<api.HttpResponse>;
|
||||
|
||||
export function SendSocketMessage(arg1:string,arg2:string):Promise<boolean>;
|
||||
|
||||
export function SetLanguage(arg1:string):Promise<boolean>;
|
||||
|
||||
export function SetLogLevel(arg1:string):Promise<boolean>;
|
||||
|
||||
export function WindowClose():Promise<void>;
|
||||
|
||||
export function WindowMinimise():Promise<void>;
|
||||
|
||||
export function WindowToggleMaximise():Promise<void>;
|
||||
|
||||
export function WriteFileContent(arg1:string,arg2:string):Promise<boolean>;
|
||||
@@ -0,0 +1,139 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function ClearRequestLogs() {
|
||||
return window['go']['service']['App']['ClearRequestLogs']();
|
||||
}
|
||||
|
||||
export function ConnectSocket(arg1, arg2, arg3, arg4, arg5) {
|
||||
return window['go']['service']['App']['ConnectSocket'](arg1, arg2, arg3, arg4, arg5);
|
||||
}
|
||||
|
||||
export function DeleteRequestLog(arg1) {
|
||||
return window['go']['service']['App']['DeleteRequestLog'](arg1);
|
||||
}
|
||||
|
||||
export function DisconnectSocket(arg1) {
|
||||
return window['go']['service']['App']['DisconnectSocket'](arg1);
|
||||
}
|
||||
|
||||
export function ExecuteJs(arg1) {
|
||||
return window['go']['service']['App']['ExecuteJs'](arg1);
|
||||
}
|
||||
|
||||
export function Flashtime() {
|
||||
return window['go']['service']['App']['Flashtime']();
|
||||
}
|
||||
|
||||
export function GetALLLang() {
|
||||
return window['go']['service']['App']['GetALLLang']();
|
||||
}
|
||||
|
||||
export function GetCurrentLang() {
|
||||
return window['go']['service']['App']['GetCurrentLang']();
|
||||
}
|
||||
|
||||
export function GetLangPack() {
|
||||
return window['go']['service']['App']['GetLangPack']();
|
||||
}
|
||||
|
||||
export function GetLangTextMap() {
|
||||
return window['go']['service']['App']['GetLangTextMap']();
|
||||
}
|
||||
|
||||
export function GetLogFileContent(arg1) {
|
||||
return window['go']['service']['App']['GetLogFileContent'](arg1);
|
||||
}
|
||||
|
||||
export function GetLogFiles() {
|
||||
return window['go']['service']['App']['GetLogFiles']();
|
||||
}
|
||||
|
||||
export function GetLogLevel() {
|
||||
return window['go']['service']['App']['GetLogLevel']();
|
||||
}
|
||||
|
||||
export function GetProcessName() {
|
||||
return window['go']['service']['App']['GetProcessName']();
|
||||
}
|
||||
|
||||
export function GetRequestLog(arg1) {
|
||||
return window['go']['service']['App']['GetRequestLog'](arg1);
|
||||
}
|
||||
|
||||
export function GetRequestLogs() {
|
||||
return window['go']['service']['App']['GetRequestLogs']();
|
||||
}
|
||||
|
||||
export function GetSystemInfo() {
|
||||
return window['go']['service']['App']['GetSystemInfo']();
|
||||
}
|
||||
|
||||
export function Gettestjson() {
|
||||
return window['go']['service']['App']['Gettestjson']();
|
||||
}
|
||||
|
||||
export function Greet(arg1) {
|
||||
return window['go']['service']['App']['Greet'](arg1);
|
||||
}
|
||||
|
||||
export function IsSocketConnected(arg1) {
|
||||
return window['go']['service']['App']['IsSocketConnected'](arg1);
|
||||
}
|
||||
|
||||
export function Notify(arg1, arg2) {
|
||||
return window['go']['service']['App']['Notify'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function OpenFileSelect() {
|
||||
return window['go']['service']['App']['OpenFileSelect']();
|
||||
}
|
||||
|
||||
export function OpenFolderSelect() {
|
||||
return window['go']['service']['App']['OpenFolderSelect']();
|
||||
}
|
||||
|
||||
export function ReadFileContent(arg1) {
|
||||
return window['go']['service']['App']['ReadFileContent'](arg1);
|
||||
}
|
||||
|
||||
export function SaveFileSelect() {
|
||||
return window['go']['service']['App']['SaveFileSelect']();
|
||||
}
|
||||
|
||||
export function SearchRequestLogs(arg1) {
|
||||
return window['go']['service']['App']['SearchRequestLogs'](arg1);
|
||||
}
|
||||
|
||||
export function SendHttpRequest(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12) {
|
||||
return window['go']['service']['App']['SendHttpRequest'](arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12);
|
||||
}
|
||||
|
||||
export function SendSocketMessage(arg1, arg2) {
|
||||
return window['go']['service']['App']['SendSocketMessage'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function SetLanguage(arg1) {
|
||||
return window['go']['service']['App']['SetLanguage'](arg1);
|
||||
}
|
||||
|
||||
export function SetLogLevel(arg1) {
|
||||
return window['go']['service']['App']['SetLogLevel'](arg1);
|
||||
}
|
||||
|
||||
export function WindowClose() {
|
||||
return window['go']['service']['App']['WindowClose']();
|
||||
}
|
||||
|
||||
export function WindowMinimise() {
|
||||
return window['go']['service']['App']['WindowMinimise']();
|
||||
}
|
||||
|
||||
export function WindowToggleMaximise() {
|
||||
return window['go']['service']['App']['WindowToggleMaximise']();
|
||||
}
|
||||
|
||||
export function WriteFileContent(arg1, arg2) {
|
||||
return window['go']['service']['App']['WriteFileContent'](arg1, arg2);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@wailsapp/runtime",
|
||||
"version": "2.0.0",
|
||||
"description": "Wails Javascript runtime library",
|
||||
"main": "runtime.js",
|
||||
"types": "runtime.d.ts",
|
||||
"scripts": {
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wailsapp/wails.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Wails",
|
||||
"Javascript",
|
||||
"Go"
|
||||
],
|
||||
"author": "Lea Anthony <lea.anthony@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wailsapp/wails/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||
}
|
||||
+330
@@ -0,0 +1,330 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export interface Screen {
|
||||
isCurrent: boolean;
|
||||
isPrimary: boolean;
|
||||
width : number
|
||||
height : number
|
||||
}
|
||||
|
||||
// Environment information such as platform, buildtype, ...
|
||||
export interface EnvironmentInfo {
|
||||
buildType: string;
|
||||
platform: string;
|
||||
arch: string;
|
||||
}
|
||||
|
||||
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
|
||||
// emits the given event. Optional data may be passed with the event.
|
||||
// This will trigger any event listeners.
|
||||
export function EventsEmit(eventName: string, ...data: any): void;
|
||||
|
||||
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
|
||||
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
|
||||
// sets up a listener for the given event name, but will only trigger a given number times.
|
||||
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
|
||||
|
||||
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
|
||||
// sets up a listener for the given event name, but will only trigger once.
|
||||
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
|
||||
// unregisters the listener for the given event name.
|
||||
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
|
||||
|
||||
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
|
||||
// unregisters all listeners.
|
||||
export function EventsOffAll(): void;
|
||||
|
||||
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
|
||||
// logs the given message as a raw message
|
||||
export function LogPrint(message: string): void;
|
||||
|
||||
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
|
||||
// logs the given message at the `trace` log level.
|
||||
export function LogTrace(message: string): void;
|
||||
|
||||
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
|
||||
// logs the given message at the `debug` log level.
|
||||
export function LogDebug(message: string): void;
|
||||
|
||||
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
|
||||
// logs the given message at the `error` log level.
|
||||
export function LogError(message: string): void;
|
||||
|
||||
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
|
||||
// logs the given message at the `fatal` log level.
|
||||
// The application will quit after calling this method.
|
||||
export function LogFatal(message: string): void;
|
||||
|
||||
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
|
||||
// logs the given message at the `info` log level.
|
||||
export function LogInfo(message: string): void;
|
||||
|
||||
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
|
||||
// logs the given message at the `warning` log level.
|
||||
export function LogWarning(message: string): void;
|
||||
|
||||
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
|
||||
// Forces a reload by the main application as well as connected browsers.
|
||||
export function WindowReload(): void;
|
||||
|
||||
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
|
||||
// Reloads the application frontend.
|
||||
export function WindowReloadApp(): void;
|
||||
|
||||
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
|
||||
// Sets the window AlwaysOnTop or not on top.
|
||||
export function WindowSetAlwaysOnTop(b: boolean): void;
|
||||
|
||||
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
|
||||
// *Windows only*
|
||||
// Sets window theme to system default (dark/light).
|
||||
export function WindowSetSystemDefaultTheme(): void;
|
||||
|
||||
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
|
||||
// *Windows only*
|
||||
// Sets window to light theme.
|
||||
export function WindowSetLightTheme(): void;
|
||||
|
||||
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
|
||||
// *Windows only*
|
||||
// Sets window to dark theme.
|
||||
export function WindowSetDarkTheme(): void;
|
||||
|
||||
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
|
||||
// Centers the window on the monitor the window is currently on.
|
||||
export function WindowCenter(): void;
|
||||
|
||||
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
|
||||
// Sets the text in the window title bar.
|
||||
export function WindowSetTitle(title: string): void;
|
||||
|
||||
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
|
||||
// Makes the window full screen.
|
||||
export function WindowFullscreen(): void;
|
||||
|
||||
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
|
||||
// Restores the previous window dimensions and position prior to full screen.
|
||||
export function WindowUnfullscreen(): void;
|
||||
|
||||
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
|
||||
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
|
||||
export function WindowIsFullscreen(): Promise<boolean>;
|
||||
|
||||
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||
// Sets the width and height of the window.
|
||||
export function WindowSetSize(width: number, height: number): void;
|
||||
|
||||
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||
// Gets the width and height of the window.
|
||||
export function WindowGetSize(): Promise<Size>;
|
||||
|
||||
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
|
||||
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMaxSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
|
||||
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMinSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
|
||||
// Sets the window position relative to the monitor the window is currently on.
|
||||
export function WindowSetPosition(x: number, y: number): void;
|
||||
|
||||
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
|
||||
// Gets the window position relative to the monitor the window is currently on.
|
||||
export function WindowGetPosition(): Promise<Position>;
|
||||
|
||||
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
|
||||
// Hides the window.
|
||||
export function WindowHide(): void;
|
||||
|
||||
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
|
||||
// Shows the window, if it is currently hidden.
|
||||
export function WindowShow(): void;
|
||||
|
||||
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
|
||||
// Maximises the window to fill the screen.
|
||||
export function WindowMaximise(): void;
|
||||
|
||||
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
|
||||
// Toggles between Maximised and UnMaximised.
|
||||
export function WindowToggleMaximise(): void;
|
||||
|
||||
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
|
||||
// Restores the window to the dimensions and position prior to maximising.
|
||||
export function WindowUnmaximise(): void;
|
||||
|
||||
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
|
||||
// Returns the state of the window, i.e. whether the window is maximised or not.
|
||||
export function WindowIsMaximised(): Promise<boolean>;
|
||||
|
||||
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
|
||||
// Minimises the window.
|
||||
export function WindowMinimise(): void;
|
||||
|
||||
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
|
||||
// Restores the window to the dimensions and position prior to minimising.
|
||||
export function WindowUnminimise(): void;
|
||||
|
||||
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
|
||||
// Returns the state of the window, i.e. whether the window is minimised or not.
|
||||
export function WindowIsMinimised(): Promise<boolean>;
|
||||
|
||||
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
|
||||
// Returns the state of the window, i.e. whether the window is normal or not.
|
||||
export function WindowIsNormal(): Promise<boolean>;
|
||||
|
||||
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
|
||||
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
|
||||
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
|
||||
|
||||
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
|
||||
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
|
||||
export function ScreenGetAll(): Promise<Screen[]>;
|
||||
|
||||
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
|
||||
// Opens the given URL in the system browser.
|
||||
export function BrowserOpenURL(url: string): void;
|
||||
|
||||
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
|
||||
// Returns information about the environment
|
||||
export function Environment(): Promise<EnvironmentInfo>;
|
||||
|
||||
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
|
||||
// Quits the application.
|
||||
export function Quit(): void;
|
||||
|
||||
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
|
||||
// Hides the application.
|
||||
export function Hide(): void;
|
||||
|
||||
// [Show](https://wails.io/docs/reference/runtime/intro#show)
|
||||
// Shows the application.
|
||||
export function Show(): void;
|
||||
|
||||
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
|
||||
// Returns the current text stored on clipboard
|
||||
export function ClipboardGetText(): Promise<string>;
|
||||
|
||||
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||
// Sets a text on the clipboard
|
||||
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||
|
||||
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
|
||||
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
|
||||
|
||||
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
|
||||
// OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
export function OnFileDropOff() :void
|
||||
|
||||
// Check if the file path resolver is available
|
||||
export function CanResolveFilePaths(): boolean;
|
||||
|
||||
// Resolves file paths for an array of files
|
||||
export function ResolveFilePaths(files: File[]): void
|
||||
|
||||
// Notification types
|
||||
export interface NotificationOptions {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string; // macOS and Linux only
|
||||
body?: string;
|
||||
categoryId?: string;
|
||||
data?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface NotificationAction {
|
||||
id?: string;
|
||||
title?: string;
|
||||
destructive?: boolean; // macOS-specific
|
||||
}
|
||||
|
||||
export interface NotificationCategory {
|
||||
id?: string;
|
||||
actions?: NotificationAction[];
|
||||
hasReplyField?: boolean;
|
||||
replyPlaceholder?: string;
|
||||
replyButtonTitle?: string;
|
||||
}
|
||||
|
||||
// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications)
|
||||
// Initializes the notification service for the application.
|
||||
// This must be called before sending any notifications.
|
||||
export function InitializeNotifications(): Promise<void>;
|
||||
|
||||
// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications)
|
||||
// Cleans up notification resources and releases any held connections.
|
||||
export function CleanupNotifications(): Promise<void>;
|
||||
|
||||
// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable)
|
||||
// Checks if notifications are available on the current platform.
|
||||
export function IsNotificationAvailable(): Promise<boolean>;
|
||||
|
||||
// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization)
|
||||
// Requests notification authorization from the user (macOS only).
|
||||
export function RequestNotificationAuthorization(): Promise<boolean>;
|
||||
|
||||
// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization)
|
||||
// Checks the current notification authorization status (macOS only).
|
||||
export function CheckNotificationAuthorization(): Promise<boolean>;
|
||||
|
||||
// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification)
|
||||
// Sends a basic notification with the given options.
|
||||
export function SendNotification(options: NotificationOptions): Promise<void>;
|
||||
|
||||
// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions)
|
||||
// Sends a notification with action buttons. Requires a registered category.
|
||||
export function SendNotificationWithActions(options: NotificationOptions): Promise<void>;
|
||||
|
||||
// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory)
|
||||
// Registers a notification category that can be used with SendNotificationWithActions.
|
||||
export function RegisterNotificationCategory(category: NotificationCategory): Promise<void>;
|
||||
|
||||
// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory)
|
||||
// Removes a previously registered notification category.
|
||||
export function RemoveNotificationCategory(categoryId: string): Promise<void>;
|
||||
|
||||
// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications)
|
||||
// Removes all pending notifications from the notification center.
|
||||
export function RemoveAllPendingNotifications(): Promise<void>;
|
||||
|
||||
// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification)
|
||||
// Removes a specific pending notification by its identifier.
|
||||
export function RemovePendingNotification(identifier: string): Promise<void>;
|
||||
|
||||
// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications)
|
||||
// Removes all delivered notifications from the notification center.
|
||||
export function RemoveAllDeliveredNotifications(): Promise<void>;
|
||||
|
||||
// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification)
|
||||
// Removes a specific delivered notification by its identifier.
|
||||
export function RemoveDeliveredNotification(identifier: string): Promise<void>;
|
||||
|
||||
// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification)
|
||||
// Removes a notification by its identifier (cross-platform convenience function).
|
||||
export function RemoveNotification(identifier: string): Promise<void>;
|
||||
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export function LogPrint(message) {
|
||||
window.runtime.LogPrint(message);
|
||||
}
|
||||
|
||||
export function LogTrace(message) {
|
||||
window.runtime.LogTrace(message);
|
||||
}
|
||||
|
||||
export function LogDebug(message) {
|
||||
window.runtime.LogDebug(message);
|
||||
}
|
||||
|
||||
export function LogInfo(message) {
|
||||
window.runtime.LogInfo(message);
|
||||
}
|
||||
|
||||
export function LogWarning(message) {
|
||||
window.runtime.LogWarning(message);
|
||||
}
|
||||
|
||||
export function LogError(message) {
|
||||
window.runtime.LogError(message);
|
||||
}
|
||||
|
||||
export function LogFatal(message) {
|
||||
window.runtime.LogFatal(message);
|
||||
}
|
||||
|
||||
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
|
||||
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||
}
|
||||
|
||||
export function EventsOn(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, -1);
|
||||
}
|
||||
|
||||
export function EventsOff(eventName, ...additionalEventNames) {
|
||||
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||
}
|
||||
|
||||
export function EventsOffAll() {
|
||||
return window.runtime.EventsOffAll();
|
||||
}
|
||||
|
||||
export function EventsOnce(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, 1);
|
||||
}
|
||||
|
||||
export function EventsEmit(eventName) {
|
||||
let args = [eventName].slice.call(arguments);
|
||||
return window.runtime.EventsEmit.apply(null, args);
|
||||
}
|
||||
|
||||
export function WindowReload() {
|
||||
window.runtime.WindowReload();
|
||||
}
|
||||
|
||||
export function WindowReloadApp() {
|
||||
window.runtime.WindowReloadApp();
|
||||
}
|
||||
|
||||
export function WindowSetAlwaysOnTop(b) {
|
||||
window.runtime.WindowSetAlwaysOnTop(b);
|
||||
}
|
||||
|
||||
export function WindowSetSystemDefaultTheme() {
|
||||
window.runtime.WindowSetSystemDefaultTheme();
|
||||
}
|
||||
|
||||
export function WindowSetLightTheme() {
|
||||
window.runtime.WindowSetLightTheme();
|
||||
}
|
||||
|
||||
export function WindowSetDarkTheme() {
|
||||
window.runtime.WindowSetDarkTheme();
|
||||
}
|
||||
|
||||
export function WindowCenter() {
|
||||
window.runtime.WindowCenter();
|
||||
}
|
||||
|
||||
export function WindowSetTitle(title) {
|
||||
window.runtime.WindowSetTitle(title);
|
||||
}
|
||||
|
||||
export function WindowFullscreen() {
|
||||
window.runtime.WindowFullscreen();
|
||||
}
|
||||
|
||||
export function WindowUnfullscreen() {
|
||||
window.runtime.WindowUnfullscreen();
|
||||
}
|
||||
|
||||
export function WindowIsFullscreen() {
|
||||
return window.runtime.WindowIsFullscreen();
|
||||
}
|
||||
|
||||
export function WindowGetSize() {
|
||||
return window.runtime.WindowGetSize();
|
||||
}
|
||||
|
||||
export function WindowSetSize(width, height) {
|
||||
window.runtime.WindowSetSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMaxSize(width, height) {
|
||||
window.runtime.WindowSetMaxSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMinSize(width, height) {
|
||||
window.runtime.WindowSetMinSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetPosition(x, y) {
|
||||
window.runtime.WindowSetPosition(x, y);
|
||||
}
|
||||
|
||||
export function WindowGetPosition() {
|
||||
return window.runtime.WindowGetPosition();
|
||||
}
|
||||
|
||||
export function WindowHide() {
|
||||
window.runtime.WindowHide();
|
||||
}
|
||||
|
||||
export function WindowShow() {
|
||||
window.runtime.WindowShow();
|
||||
}
|
||||
|
||||
export function WindowMaximise() {
|
||||
window.runtime.WindowMaximise();
|
||||
}
|
||||
|
||||
export function WindowToggleMaximise() {
|
||||
window.runtime.WindowToggleMaximise();
|
||||
}
|
||||
|
||||
export function WindowUnmaximise() {
|
||||
window.runtime.WindowUnmaximise();
|
||||
}
|
||||
|
||||
export function WindowIsMaximised() {
|
||||
return window.runtime.WindowIsMaximised();
|
||||
}
|
||||
|
||||
export function WindowMinimise() {
|
||||
window.runtime.WindowMinimise();
|
||||
}
|
||||
|
||||
export function WindowUnminimise() {
|
||||
window.runtime.WindowUnminimise();
|
||||
}
|
||||
|
||||
export function WindowSetBackgroundColour(R, G, B, A) {
|
||||
window.runtime.WindowSetBackgroundColour(R, G, B, A);
|
||||
}
|
||||
|
||||
export function ScreenGetAll() {
|
||||
return window.runtime.ScreenGetAll();
|
||||
}
|
||||
|
||||
export function WindowIsMinimised() {
|
||||
return window.runtime.WindowIsMinimised();
|
||||
}
|
||||
|
||||
export function WindowIsNormal() {
|
||||
return window.runtime.WindowIsNormal();
|
||||
}
|
||||
|
||||
export function BrowserOpenURL(url) {
|
||||
window.runtime.BrowserOpenURL(url);
|
||||
}
|
||||
|
||||
export function Environment() {
|
||||
return window.runtime.Environment();
|
||||
}
|
||||
|
||||
export function Quit() {
|
||||
window.runtime.Quit();
|
||||
}
|
||||
|
||||
export function Hide() {
|
||||
window.runtime.Hide();
|
||||
}
|
||||
|
||||
export function Show() {
|
||||
window.runtime.Show();
|
||||
}
|
||||
|
||||
export function ClipboardGetText() {
|
||||
return window.runtime.ClipboardGetText();
|
||||
}
|
||||
|
||||
export function ClipboardSetText(text) {
|
||||
return window.runtime.ClipboardSetText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
*
|
||||
* @export
|
||||
* @callback OnFileDropCallback
|
||||
* @param {number} x - x coordinate of the drop
|
||||
* @param {number} y - y coordinate of the drop
|
||||
* @param {string[]} paths - A list of file paths.
|
||||
*/
|
||||
|
||||
/**
|
||||
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
*
|
||||
* @export
|
||||
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
|
||||
*/
|
||||
export function OnFileDrop(callback, useDropTarget) {
|
||||
return window.runtime.OnFileDrop(callback, useDropTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
*/
|
||||
export function OnFileDropOff() {
|
||||
return window.runtime.OnFileDropOff();
|
||||
}
|
||||
|
||||
export function CanResolveFilePaths() {
|
||||
return window.runtime.CanResolveFilePaths();
|
||||
}
|
||||
|
||||
export function ResolveFilePaths(files) {
|
||||
return window.runtime.ResolveFilePaths(files);
|
||||
}
|
||||
|
||||
export function InitializeNotifications() {
|
||||
return window.runtime.InitializeNotifications();
|
||||
}
|
||||
|
||||
export function CleanupNotifications() {
|
||||
return window.runtime.CleanupNotifications();
|
||||
}
|
||||
|
||||
export function IsNotificationAvailable() {
|
||||
return window.runtime.IsNotificationAvailable();
|
||||
}
|
||||
|
||||
export function RequestNotificationAuthorization() {
|
||||
return window.runtime.RequestNotificationAuthorization();
|
||||
}
|
||||
|
||||
export function CheckNotificationAuthorization() {
|
||||
return window.runtime.CheckNotificationAuthorization();
|
||||
}
|
||||
|
||||
export function SendNotification(options) {
|
||||
return window.runtime.SendNotification(options);
|
||||
}
|
||||
|
||||
export function SendNotificationWithActions(options) {
|
||||
return window.runtime.SendNotificationWithActions(options);
|
||||
}
|
||||
|
||||
export function RegisterNotificationCategory(category) {
|
||||
return window.runtime.RegisterNotificationCategory(category);
|
||||
}
|
||||
|
||||
export function RemoveNotificationCategory(categoryId) {
|
||||
return window.runtime.RemoveNotificationCategory(categoryId);
|
||||
}
|
||||
|
||||
export function RemoveAllPendingNotifications() {
|
||||
return window.runtime.RemoveAllPendingNotifications();
|
||||
}
|
||||
|
||||
export function RemovePendingNotification(identifier) {
|
||||
return window.runtime.RemovePendingNotification(identifier);
|
||||
}
|
||||
|
||||
export function RemoveAllDeliveredNotifications() {
|
||||
return window.runtime.RemoveAllDeliveredNotifications();
|
||||
}
|
||||
|
||||
export function RemoveDeliveredNotification(identifier) {
|
||||
return window.runtime.RemoveDeliveredNotification(identifier);
|
||||
}
|
||||
|
||||
export function RemoveNotification(identifier) {
|
||||
return window.runtime.RemoveNotification(identifier);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build debug
|
||||
|
||||
package global
|
||||
|
||||
// IsDebug 标记是否为调试模式
|
||||
// 仅在使用 -tags debug 编译时为 true
|
||||
var IsDebug = true
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !debug
|
||||
|
||||
package global
|
||||
|
||||
// IsDebug 标记是否为调试模式
|
||||
// 编译后为 false,不允许使用 debug 模式
|
||||
var IsDebug = false
|
||||
@@ -0,0 +1,11 @@
|
||||
package global
|
||||
|
||||
type GConfig struct {
|
||||
Language string `json:"language"`
|
||||
LogDir string `json:"log_dir"`
|
||||
}
|
||||
|
||||
var GlobalConfig = &GConfig{
|
||||
Language: "zh-CN",
|
||||
LogDir: "logs",
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package global
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
Pa "path"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
// 1. 创建必要目录
|
||||
dirs := []string{
|
||||
"lang",
|
||||
GlobalConfig.LogDir,
|
||||
}
|
||||
for _, dir := range dirs {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
os.MkdirAll(dir, 0755)
|
||||
}
|
||||
}
|
||||
|
||||
InitLogger()
|
||||
Log.Info("日志系统初始化完成")
|
||||
|
||||
InitLang()
|
||||
Log.Infof("语言系统初始化完成,当前语言: %s", useLangPath)
|
||||
}
|
||||
|
||||
func GetLangTextMap() map[string]string {
|
||||
langPack, err := GetLangPack()
|
||||
if err != nil {
|
||||
Log.Warnf("获取语言包失败: %v", err)
|
||||
return make(map[string]string)
|
||||
}
|
||||
return langPack.Textmap
|
||||
}
|
||||
|
||||
func GetLangPack() (*LanguagePack, error) {
|
||||
langPath := Pa.Join(pathLang, useLangPath)
|
||||
|
||||
if cached, ok := langPackCache[langPath]; ok {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
if _, err := os.Stat(langPath); err == nil {
|
||||
pack, err := tryLoadLangPack(langPath)
|
||||
if err == nil {
|
||||
langPackCache[langPath] = pack
|
||||
}
|
||||
return pack, err
|
||||
}
|
||||
|
||||
embedPath := Pa.Join("lang", useLangPath)
|
||||
pack, err := tryLoadLangPackFromEmbed(embedPath)
|
||||
if err == nil {
|
||||
langPackCache[langPath] = pack
|
||||
}
|
||||
return pack, err
|
||||
}
|
||||
|
||||
func tryLoadLangPack(langPath string) (*LanguagePack, error) {
|
||||
infoPath := Pa.Join(langPath, "info.json")
|
||||
infoData, err := os.ReadFile(infoPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var langInfo LanguageInfo
|
||||
if err := json.Unmarshal(infoData, &langInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
textmapPath := Pa.Join(langPath, "textmap.json")
|
||||
textmapData, err := os.ReadFile(textmapPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取 textmap.json 失败: %v", err)
|
||||
}
|
||||
|
||||
var textmap map[string]string
|
||||
if err := json.Unmarshal(textmapData, &textmap); err != nil {
|
||||
return nil, fmt.Errorf("解析 textmap.json 失败: %v", err)
|
||||
}
|
||||
|
||||
return &LanguagePack{
|
||||
LanguageInfo: langInfo,
|
||||
Textmap: textmap,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func tryLoadLangPackFromEmbed(langPath string) (*LanguagePack, error) {
|
||||
if LangFS == nil {
|
||||
return nil, fmt.Errorf("嵌入的文件系统未初始化")
|
||||
}
|
||||
|
||||
infoPath := Pa.Join(langPath, "info.json")
|
||||
infoData, err := fs.ReadFile(LangFS, infoPath)
|
||||
if err != nil {
|
||||
Log.Warnf("读取嵌入 info.json 失败: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var langInfo LanguageInfo
|
||||
if err := json.Unmarshal(infoData, &langInfo); err != nil {
|
||||
Log.Warnf("解析嵌入 info.json 失败: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
textmapPath := Pa.Join(langPath, "textmap.json")
|
||||
textmapData, err := fs.ReadFile(LangFS, textmapPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取嵌入 textmap.json 失败: %v", err)
|
||||
}
|
||||
|
||||
var textmap map[string]string
|
||||
if err := json.Unmarshal(textmapData, &textmap); err != nil {
|
||||
return nil, fmt.Errorf("解析嵌入 textmap.json 失败: %v", err)
|
||||
}
|
||||
|
||||
return &LanguagePack{
|
||||
LanguageInfo: langInfo,
|
||||
Textmap: textmap,
|
||||
}, nil
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
package global
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/fs"
|
||||
"os"
|
||||
Pa "path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var (
|
||||
pathLang = "lang/"
|
||||
useLangPath = "default"
|
||||
allLangInfo = []LanguageInfo{}
|
||||
LangFS fs.FS
|
||||
langPackCache = map[string]*LanguagePack{}
|
||||
langCodeToDir = map[string]string{}
|
||||
)
|
||||
|
||||
type LanguageInfo struct {
|
||||
LanguageName string `json:"language_name"`
|
||||
LanguageCode string `json:"language_code"`
|
||||
TextmapPath string `json:"textmap_path"`
|
||||
TranslationProgress string `json:"translation_progress"`
|
||||
Translator string `json:"translator"`
|
||||
LastUpdated string `json:"last_updated"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type LanguagePack struct {
|
||||
LanguageInfo
|
||||
Textmap map[string]string `json:"textmap"`
|
||||
}
|
||||
|
||||
func InitLang() {
|
||||
useLang := GlobalConfig.Language
|
||||
allLangInfo = []LanguageInfo{}
|
||||
langCodeToDir = map[string]string{}
|
||||
|
||||
// 扫描文件系统语言目录
|
||||
scanFileSystemLangs(langCodeToDir)
|
||||
// 扫描嵌入文件系统语言
|
||||
scanEmbeddedLangs(langCodeToDir)
|
||||
|
||||
// 设置当前语言路径
|
||||
if dir, ok := langCodeToDir[useLang]; ok {
|
||||
Log.Infof("找到匹配语言: %s -> %s", useLang, dir)
|
||||
useLangPath = dir
|
||||
} else {
|
||||
Log.Infof("未找到匹配语言 %s,使用默认语言", useLang)
|
||||
useLangPath = "default"
|
||||
}
|
||||
}
|
||||
|
||||
func scanFileSystemLangs(langDirMap map[string]string) {
|
||||
err := filepath.WalkDir(pathLang, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
Log.Warnf("读取语言文件夹失败: %v", err)
|
||||
return nil
|
||||
}
|
||||
if !d.IsDir() || path == pathLang {
|
||||
return nil
|
||||
}
|
||||
return loadLangInfoFromFile(path, langDirMap, "[文件系统]")
|
||||
})
|
||||
if err != nil {
|
||||
Log.Warnf("遍历文件系统 lang 目录出错: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func scanEmbeddedLangs(langDirMap map[string]string) {
|
||||
if LangFS == nil {
|
||||
Log.Warnf("读取语言文件夹失败: %v", "LangFS 为空")
|
||||
return
|
||||
}
|
||||
|
||||
entries, err := fs.ReadDir(LangFS, "lang")
|
||||
if err != nil {
|
||||
Log.Warnf("读取语言文件夹失败: %v", err)
|
||||
return
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
loadLangInfoFromEmbed(entry.Name(), langDirMap, "[嵌入FS]")
|
||||
}
|
||||
}
|
||||
|
||||
func loadLangInfoFromFile(path string, langDirMap map[string]string, source string) error {
|
||||
infoPath := Pa.Join(path, "info.json")
|
||||
data, err := os.ReadFile(infoPath)
|
||||
if err != nil {
|
||||
Log.Warnf("读取语言文件夹失败: %v", err)
|
||||
return nil
|
||||
}
|
||||
return parseAndAddLangInfo(data, filepath.Base(path), langDirMap, source)
|
||||
}
|
||||
|
||||
func loadLangInfoFromEmbed(path string, langDirMap map[string]string, source string) {
|
||||
infoPath := Pa.Join("lang", path, "info.json")
|
||||
data, err := fs.ReadFile(LangFS, infoPath)
|
||||
if err != nil {
|
||||
Log.Warnf("读取语言文件夹失败: %v", err)
|
||||
return
|
||||
}
|
||||
parseAndAddLangInfo(data, filepath.Base(path), langDirMap, source)
|
||||
}
|
||||
|
||||
func parseAndAddLangInfo(data []byte, dirName string, langDirMap map[string]string, source string) error {
|
||||
var info LanguageInfo
|
||||
if err := json.Unmarshal(data, &info); err != nil {
|
||||
Log.Warnf("解析语言文件夹失败: %v", err)
|
||||
return nil
|
||||
}
|
||||
if info.LanguageCode == "" || info.TextmapPath == "" {
|
||||
Log.Warnf("语言信息不完整: %s", dirName)
|
||||
return nil
|
||||
}
|
||||
if !containsLang(allLangInfo, info.LanguageCode) {
|
||||
allLangInfo = append(allLangInfo, info)
|
||||
langDirMap[info.LanguageCode] = dirName
|
||||
Log.Debugf("%s 识别到语言: %s (%s) -> %s", source, info.LanguageName, info.LanguageCode, dirName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func containsLang(slice []LanguageInfo, code string) bool {
|
||||
for _, s := range slice {
|
||||
if s.LanguageCode == code {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ClearLangCache() {
|
||||
langPackCache = map[string]*LanguagePack{}
|
||||
}
|
||||
|
||||
func GetLangInfoList() []LanguageInfo {
|
||||
return allLangInfo
|
||||
}
|
||||
|
||||
func UpdateCurrentLangPath() {
|
||||
useLang := GlobalConfig.Language
|
||||
if dir, ok := langCodeToDir[useLang]; ok {
|
||||
Log.Infof("更新语言路径: %s -> %s", useLang, dir)
|
||||
useLangPath = dir
|
||||
} else {
|
||||
Log.Infof("未找到匹配语言 %s,使用默认语言", useLang)
|
||||
useLangPath = "default"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package global
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var Log *logrus.Logger
|
||||
|
||||
const (
|
||||
red = 31
|
||||
yellow = 33
|
||||
blue = 36
|
||||
gray = 37
|
||||
)
|
||||
|
||||
// LogFormatter 控制台日志格式化器(带颜色)
|
||||
type LogFormatter struct{}
|
||||
|
||||
func (f *LogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||
var levelColor int
|
||||
switch entry.Level {
|
||||
case logrus.DebugLevel, logrus.TraceLevel:
|
||||
levelColor = gray
|
||||
case logrus.WarnLevel:
|
||||
levelColor = yellow
|
||||
case logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel:
|
||||
levelColor = red
|
||||
default:
|
||||
levelColor = blue
|
||||
}
|
||||
|
||||
var b *bytes.Buffer
|
||||
if entry.Buffer != nil {
|
||||
b = entry.Buffer
|
||||
} else {
|
||||
b = &bytes.Buffer{}
|
||||
}
|
||||
|
||||
timestamp := entry.Time.Format("2006/01/02 15:04:05")
|
||||
levelStr := strings.ToUpper(entry.Level.String())
|
||||
|
||||
if IsDebug && entry.HasCaller() {
|
||||
funcVal := entry.Caller.Function
|
||||
fileVal := fmt.Sprintf("%s:%d", path.Base(entry.Caller.File), entry.Caller.Line)
|
||||
fmt.Fprintf(b, "\033[%dm[%s]\033[0m [%s] \033[%dm[%s]\033[0m %s %s: %s\n",
|
||||
colorGreen, processName, timestamp, levelColor, levelStr, funcVal, fileVal, entry.Message)
|
||||
} else {
|
||||
fmt.Fprintf(b, "\033[%dm[%s]\033[0m [%s] \033[%dm[%s]\033[0m %s\n",
|
||||
colorGreen, processName, timestamp, levelColor, levelStr, entry.Message)
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
// FileFormatter 文件日志格式化器(不包含 ANSI 颜色代码)
|
||||
type FileFormatter struct{}
|
||||
|
||||
func (f *FileFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||
var b *bytes.Buffer
|
||||
if entry.Buffer != nil {
|
||||
b = entry.Buffer
|
||||
} else {
|
||||
b = &bytes.Buffer{}
|
||||
}
|
||||
|
||||
timestamp := entry.Time.Format("2006/01/02 15:04:05")
|
||||
levelStr := strings.ToUpper(entry.Level.String())
|
||||
|
||||
if IsDebug && entry.HasCaller() {
|
||||
funcVal := entry.Caller.Function
|
||||
fileVal := fmt.Sprintf("%s:%d", path.Base(entry.Caller.File), entry.Caller.Line)
|
||||
fmt.Fprintf(b, "[%s] [%s] [%s] %s %s: %s\n",
|
||||
processName, timestamp, levelStr, funcVal, fileVal, entry.Message)
|
||||
} else {
|
||||
fmt.Fprintf(b, "[%s] [%s] [%s] %s\n",
|
||||
processName, timestamp, levelStr, entry.Message)
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
var processName string
|
||||
|
||||
func init() {
|
||||
processName = filepath.Base(os.Args[0])
|
||||
processName = strings.TrimSuffix(processName, filepath.Ext(processName))
|
||||
}
|
||||
|
||||
func GetProcessName() string {
|
||||
return processName
|
||||
}
|
||||
|
||||
// SetLogLevel 动态设置日志等级
|
||||
func SetLogLevel(level logrus.Level) {
|
||||
if Log != nil {
|
||||
Log.SetLevel(level)
|
||||
}
|
||||
}
|
||||
|
||||
// InitLogger 初始化日志系统,每次运行时按启动时间保存日志文件,自动清理旧日志
|
||||
func InitLogger() {
|
||||
Log = logrus.New()
|
||||
Log.SetFormatter(&LogFormatter{})
|
||||
Log.SetLevel(logrus.InfoLevel)
|
||||
|
||||
if IsDebug {
|
||||
Log.SetReportCaller(true)
|
||||
} else {
|
||||
Log.SetReportCaller(false)
|
||||
}
|
||||
|
||||
logDir := GlobalConfig.LogDir
|
||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||
Log.Warnf("无法创建日志目录: %v", err)
|
||||
}
|
||||
|
||||
cleanOldLogs(logDir, 10)
|
||||
|
||||
startTime := time.Now().Format("2006-01-02_15-04-05")
|
||||
logFile := filepath.Join(logDir, startTime+".log")
|
||||
file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
Log.Warnf("无法打开日志文件: %v", err)
|
||||
} else {
|
||||
Log.SetOutput(os.Stdout)
|
||||
Log.AddHook(&FileHook{file: file})
|
||||
}
|
||||
}
|
||||
|
||||
const colorGreen = 32
|
||||
|
||||
// FileHook 将日志同时写入文件的 Hook
|
||||
type FileHook struct {
|
||||
file *os.File
|
||||
}
|
||||
|
||||
func (h *FileHook) Levels() []logrus.Level {
|
||||
return logrus.AllLevels
|
||||
}
|
||||
|
||||
func (h *FileHook) Fire(entry *logrus.Entry) error {
|
||||
formatter := &FileFormatter{}
|
||||
line, err := formatter.Format(entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = h.file.Write(line)
|
||||
return err
|
||||
}
|
||||
|
||||
// cleanOldLogs 清理旧日志文件,保留最新的 maxKeep 个日志
|
||||
func cleanOldLogs(logDir string, maxKeep int) {
|
||||
entries, err := os.ReadDir(logDir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var logFiles []os.DirEntry
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".log") {
|
||||
logFiles = append(logFiles, entry)
|
||||
}
|
||||
}
|
||||
|
||||
if len(logFiles) <= maxKeep {
|
||||
return
|
||||
}
|
||||
|
||||
// 文件名包含时间戳,按文件名排序即按时间排序
|
||||
sort.Slice(logFiles, func(i, j int) bool {
|
||||
return logFiles[i].Name() < logFiles[j].Name()
|
||||
})
|
||||
|
||||
filesToDelete := logFiles[:len(logFiles)-maxKeep]
|
||||
for _, file := range filesToDelete {
|
||||
os.Remove(filepath.Join(logDir, file.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
func GetRuntimeInfo() string {
|
||||
return fmt.Sprintf("Go %s, OS %s, Arch %s", runtime.Version(), runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package global
|
||||
|
||||
// Version 版本号,通过 ldflags 注入
|
||||
// 编译时: wails build -ldflags "-X webhelper/global.Version=v1.2.3"
|
||||
// 开发时默认为 "dev"
|
||||
var Version = "dev"
|
||||
@@ -0,0 +1,45 @@
|
||||
module webhelper
|
||||
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/wailsapp/wails/v2 v2.12.0
|
||||
)
|
||||
|
||||
require (
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||
github.com/leaanthony/gosod v1.0.4 // indirect
|
||||
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||
github.com/leaanthony/u v1.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/samber/lo v1.49.1 // indirect
|
||||
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.22 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
)
|
||||
|
||||
// replace github.com/wailsapp/wails/v2 v2.10.1 => C:\Users\mogum\go\pkg\mod
|
||||
@@ -0,0 +1,104 @@
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
|
||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c h1:OcLmPfx1T1RmZVHHFwWMPaZDdRf0DBMZOFMVWJa7Pdk=
|
||||
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
||||
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
|
||||
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
|
||||
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
|
||||
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
||||
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
|
||||
github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
github.com/wailsapp/wails/v2 v2.12.0 h1:BHO/kLNWFHYjCzucxbzAYZWUjub1Tvb4cSguQozHn5c=
|
||||
github.com/wailsapp/wails/v2 v2.12.0/go.mod h1:mo1bzK1DEJrobt7YrBjgxvb5Sihb1mhAY09hppbibQg=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"language_name": "简体中文",
|
||||
"language_code": "zh-CN",
|
||||
"textmap_path": "textmap.json",
|
||||
"translation_progress": "100%",
|
||||
"translator": "MoGuQAQ",
|
||||
"last_updated": "2025-07-26",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
{
|
||||
"app.tab.webdebug": "网页调试",
|
||||
"app.tab.jsdebug": "JS调试",
|
||||
"app.tab.jsonparser": "JSON解析",
|
||||
"app.tab.socketdebug": "Socket调试",
|
||||
"app.tab.settings": "程序设置",
|
||||
|
||||
"header.app_name": "WebHelper",
|
||||
|
||||
"webdebug.url_placeholder": "输入请求地址",
|
||||
"webdebug.btn_send": "发送",
|
||||
"webdebug.tab.headers": "请求头",
|
||||
"webdebug.tab.body": "请求体",
|
||||
"webdebug.tab.cookie": "Cookie",
|
||||
"webdebug.tab.advanced": "高级设置",
|
||||
"webdebug.header.key_placeholder": "Key",
|
||||
"webdebug.header.value_placeholder": "Value",
|
||||
"webdebug.header.add": "添加请求头",
|
||||
"webdebug.body.type_text": "文本",
|
||||
"webdebug.body.type_json": "JSON",
|
||||
"webdebug.body.type_form": "表单",
|
||||
"webdebug.body.type_file": "文件",
|
||||
"webdebug.body.file_placeholder": "选择文件",
|
||||
"webdebug.body.file_btn": "选择文件",
|
||||
"webdebug.body.json_placeholder": "输入 JSON 数据",
|
||||
"webdebug.body.text_placeholder": "输入请求体内容",
|
||||
"webdebug.body.cookie_placeholder": "Cookie 字符串 (格式: name1=value1; name2=value2)",
|
||||
"webdebug.advanced.proxy_label": "代理地址",
|
||||
"webdebug.advanced.proxy_placeholder": "http://127.0.0.1:7890",
|
||||
"webdebug.advanced.timeout_label": "超时时间",
|
||||
"webdebug.advanced.timeout_unit": "秒",
|
||||
"webdebug.advanced.content_type_label": "Content-Type",
|
||||
"webdebug.advanced.content_type_placeholder": "自动检测",
|
||||
"webdebug.advanced.insecure_label": "忽略证书",
|
||||
"webdebug.advanced.log_divider": "日志设置",
|
||||
"webdebug.advanced.save_log_label": "记录日志",
|
||||
"webdebug.advanced.log_manage_label": "日志管理",
|
||||
"webdebug.advanced.view_logs_btn": "查看日志记录",
|
||||
"webdebug.response.title": "响应结果",
|
||||
"webdebug.response.tab.body": "响应正文",
|
||||
"webdebug.response.tab.headers": "响应头",
|
||||
"webdebug.response.tab.more": "更多信息",
|
||||
"webdebug.response.body_placeholder": "响应内容将显示在这里",
|
||||
"webdebug.response.send_to_json": "发送到 JSON 解析器",
|
||||
"webdebug.response.headers_placeholder": "响应头将显示在这里",
|
||||
"webdebug.response.status_code_label": "状态码",
|
||||
"webdebug.response.status_label": "状态",
|
||||
"webdebug.response.content_type_label": "Content-Type",
|
||||
"webdebug.response.content_length_label": "内容长度",
|
||||
"webdebug.response.request_time_label": "请求耗时",
|
||||
"webdebug.log.title": "请求日志",
|
||||
"webdebug.log.search_placeholder": "搜索日志...",
|
||||
"webdebug.log.clear": "清空",
|
||||
"webdebug.log.empty": "暂无日志",
|
||||
"webdebug.log.tab.request_info": "请求信息",
|
||||
"webdebug.log.tab.response_info": "响应信息",
|
||||
"webdebug.log.request.method_label": "请求方法",
|
||||
"webdebug.log.request.url_label": "请求地址",
|
||||
"webdebug.log.request.host_label": "主机",
|
||||
"webdebug.log.request.path_label": "路径",
|
||||
"webdebug.log.request.body_label": "请求体",
|
||||
"webdebug.log.response.status_code_label": "状态码",
|
||||
"webdebug.log.response.status_label": "状态",
|
||||
"webdebug.log.response.size_label": "响应大小",
|
||||
"webdebug.log.response.time_label": "耗时",
|
||||
"webdebug.log.response.body_label": "响应正文",
|
||||
"webdebug.log.load_to_debug": "加载到调试页面",
|
||||
"webdebug.log.select_hint": "选择一条日志查看详情",
|
||||
"webdebug.msg.url_required": "请输入请求地址",
|
||||
"webdebug.msg.request_failed": "请求失败: ",
|
||||
"webdebug.msg.load_logs_failed": "加载日志失败: ",
|
||||
"webdebug.msg.delete_failed": "删除失败: ",
|
||||
"webdebug.msg.clear_failed": "清空失败: ",
|
||||
|
||||
"jsdebug.title": "JavaScript 调试器",
|
||||
"jsdebug.tag.duration": "耗时: ",
|
||||
"jsdebug.tag.success": "成功",
|
||||
"jsdebug.tag.failed": "失败",
|
||||
"jsdebug.btn.run": "运行",
|
||||
"jsdebug.btn.clear": "清空",
|
||||
"jsdebug.section.code": "代码",
|
||||
"jsdebug.section.output": "输出",
|
||||
"jsdebug.btn.copy": "复制",
|
||||
"jsdebug.section.return_value": "返回值",
|
||||
"jsdebug.code_placeholder": "输入 JavaScript 代码",
|
||||
"jsdebug.output_placeholder": "console.log 输出将显示在这里",
|
||||
"jsdebug.return_placeholder": "函数返回值将显示在这里",
|
||||
"jsdebug.msg.code_required": "请输入代码",
|
||||
"jsdebug.msg.execute_failed": "执行失败: ",
|
||||
"jsdebug.msg.copy_prefix": "返回值: ",
|
||||
"jsdebug.msg.copied": "已复制到剪贴板",
|
||||
"jsdebug.msg.copy_failed": "复制失败",
|
||||
|
||||
"jsonparser.title": "JSON 解析器",
|
||||
"jsonparser.stats.objects": "对象: ",
|
||||
"jsonparser.stats.arrays": "数组: ",
|
||||
"jsonparser.stats.strings": "字符串: ",
|
||||
"jsonparser.stats.numbers": "数字: ",
|
||||
"jsonparser.view.tree": "树形",
|
||||
"jsonparser.view.table": "表格",
|
||||
"jsonparser.view.raw": "原始",
|
||||
"jsonparser.btn.copy": "复制",
|
||||
"jsonparser.btn.clear": "清空",
|
||||
"jsonparser.section.input": "输入",
|
||||
"jsonparser.section.result": "解析结果",
|
||||
"jsonparser.input_placeholder": "粘贴或输入 JSON 内容",
|
||||
"jsonparser.raw_placeholder": "格式化后的 JSON 将显示在这里",
|
||||
"jsonparser.empty": "请输入 JSON 数据",
|
||||
"jsonparser.table.column.key": "Key",
|
||||
"jsonparser.table.column.index": "Index",
|
||||
"jsonparser.table.column.value": "Value",
|
||||
"jsonparser.table.column.action": "操作",
|
||||
"jsonparser.msg.parse_error": "JSON 解析错误: ",
|
||||
"jsonparser.msg.copied": "已复制到剪贴板",
|
||||
"jsonparser.msg.copy_failed": "复制失败",
|
||||
"jsonparser.msg.received": "已接收 JSON 数据",
|
||||
|
||||
"socket.connection.title": "连接配置",
|
||||
"socket.connection.status.connected": "已连接",
|
||||
"socket.connection.status.disconnected": "未连接",
|
||||
"socket.connection.btn.connect": "连接",
|
||||
"socket.connection.btn.disconnect": "断开",
|
||||
"socket.connection.label.protocol": "协议",
|
||||
"socket.connection.label.address": "地址",
|
||||
"socket.connection.label.port": "端口",
|
||||
"socket.connection.label.path": "路径",
|
||||
"socket.connection.label.headers": "请求头",
|
||||
"socket.connection.placeholder.host": "主机地址",
|
||||
"socket.connection.placeholder.port": "8080",
|
||||
"socket.connection.placeholder.path": "/",
|
||||
"socket.connection.placeholder.headers": "可选,格式: Key: Value",
|
||||
"socket.message.title": "消息记录",
|
||||
"socket.message.auto_scroll": "自动滚动",
|
||||
"socket.message.btn.clear": "清空",
|
||||
"socket.message.tag.sent": "发送",
|
||||
"socket.message.tag.received": "接收",
|
||||
"socket.message.tag.error": "错误",
|
||||
"socket.message.tag.system": "系统",
|
||||
"socket.message.empty": "暂无消息",
|
||||
"socket.message.placeholder": "输入消息内容",
|
||||
"socket.message.btn.send": "发送",
|
||||
"socket.msg.host_required": "请输入主机地址",
|
||||
"socket.msg.port_invalid": "端口格式错误",
|
||||
"socket.msg.connect_failed": "连接失败: ",
|
||||
"socket.msg.disconnect_failed": "断开连接失败: ",
|
||||
"socket.msg.not_connected": "请先连接",
|
||||
"socket.msg.send_failed": "发送失败: ",
|
||||
|
||||
"settings.title": "程序设置",
|
||||
"settings.form.language_label": "语言",
|
||||
"settings.form.proxy_divider": "代理",
|
||||
"settings.form.enable_proxy_label": "启用代理",
|
||||
"settings.form.proxy_address_label": "代理地址",
|
||||
"settings.form.proxy_address_placeholder": "127.0.0.1",
|
||||
"settings.form.proxy_port_label": "代理端口",
|
||||
"settings.form.btn.save": "保存设置",
|
||||
"settings.form.btn.reset": "恢复默认",
|
||||
"settings.msg.saved": "设置已保存",
|
||||
"settings.msg.reset": "设置已重置"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"language_name": "English",
|
||||
"language_code": "en-US",
|
||||
"textmap_path": "textmap.json",
|
||||
"translation_progress": "100%",
|
||||
"translator": "MoGuQAQ",
|
||||
"last_updated": "2025-07-26",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
{
|
||||
"app.tab.webdebug": "Web Debug",
|
||||
"app.tab.jsdebug": "JS Debug",
|
||||
"app.tab.jsonparser": "JSON Parser",
|
||||
"app.tab.socketdebug": "Socket Debug",
|
||||
"app.tab.settings": "Settings",
|
||||
|
||||
"header.app_name": "WebHelper",
|
||||
|
||||
"webdebug.url_placeholder": "Enter request URL",
|
||||
"webdebug.btn_send": "Send",
|
||||
"webdebug.tab.headers": "Headers",
|
||||
"webdebug.tab.body": "Body",
|
||||
"webdebug.tab.cookie": "Cookie",
|
||||
"webdebug.tab.advanced": "Advanced",
|
||||
"webdebug.header.key_placeholder": "Key",
|
||||
"webdebug.header.value_placeholder": "Value",
|
||||
"webdebug.header.add": "Add Header",
|
||||
"webdebug.body.type_text": "Text",
|
||||
"webdebug.body.type_json": "JSON",
|
||||
"webdebug.body.type_form": "Form",
|
||||
"webdebug.body.type_file": "File",
|
||||
"webdebug.body.file_placeholder": "Select file",
|
||||
"webdebug.body.file_btn": "Select File",
|
||||
"webdebug.body.json_placeholder": "Enter JSON data",
|
||||
"webdebug.body.text_placeholder": "Enter request body",
|
||||
"webdebug.body.cookie_placeholder": "Cookie string (format: name1=value1; name2=value2)",
|
||||
"webdebug.advanced.proxy_label": "Proxy",
|
||||
"webdebug.advanced.proxy_placeholder": "http://127.0.0.1:7890",
|
||||
"webdebug.advanced.timeout_label": "Timeout",
|
||||
"webdebug.advanced.timeout_unit": "sec",
|
||||
"webdebug.advanced.content_type_label": "Content-Type",
|
||||
"webdebug.advanced.content_type_placeholder": "Auto detect",
|
||||
"webdebug.advanced.insecure_label": "Ignore Certificate",
|
||||
"webdebug.advanced.log_divider": "Log Settings",
|
||||
"webdebug.advanced.save_log_label": "Save Log",
|
||||
"webdebug.advanced.log_manage_label": "Log Management",
|
||||
"webdebug.advanced.view_logs_btn": "View Logs",
|
||||
"webdebug.response.title": "Response",
|
||||
"webdebug.response.tab.body": "Body",
|
||||
"webdebug.response.tab.headers": "Headers",
|
||||
"webdebug.response.tab.more": "More Info",
|
||||
"webdebug.response.body_placeholder": "Response body will appear here",
|
||||
"webdebug.response.send_to_json": "Send to JSON Parser",
|
||||
"webdebug.response.headers_placeholder": "Response headers will appear here",
|
||||
"webdebug.response.status_code_label": "Status Code",
|
||||
"webdebug.response.status_label": "Status",
|
||||
"webdebug.response.content_type_label": "Content-Type",
|
||||
"webdebug.response.content_length_label": "Content Length",
|
||||
"webdebug.response.request_time_label": "Time",
|
||||
"webdebug.log.title": "Request Logs",
|
||||
"webdebug.log.search_placeholder": "Search logs...",
|
||||
"webdebug.log.clear": "Clear",
|
||||
"webdebug.log.empty": "No logs",
|
||||
"webdebug.log.tab.request_info": "Request Info",
|
||||
"webdebug.log.tab.response_info": "Response Info",
|
||||
"webdebug.log.request.method_label": "Method",
|
||||
"webdebug.log.request.url_label": "URL",
|
||||
"webdebug.log.request.host_label": "Host",
|
||||
"webdebug.log.request.path_label": "Path",
|
||||
"webdebug.log.request.body_label": "Body",
|
||||
"webdebug.log.response.status_code_label": "Status Code",
|
||||
"webdebug.log.response.status_label": "Status",
|
||||
"webdebug.log.response.size_label": "Size",
|
||||
"webdebug.log.response.time_label": "Time",
|
||||
"webdebug.log.response.body_label": "Response Body",
|
||||
"webdebug.log.load_to_debug": "Load to Debugger",
|
||||
"webdebug.log.select_hint": "Select a log to view details",
|
||||
"webdebug.msg.url_required": "Please enter a URL",
|
||||
"webdebug.msg.request_failed": "Request failed: ",
|
||||
"webdebug.msg.load_logs_failed": "Failed to load logs: ",
|
||||
"webdebug.msg.delete_failed": "Delete failed: ",
|
||||
"webdebug.msg.clear_failed": "Clear failed: ",
|
||||
|
||||
"jsdebug.title": "JavaScript Debugger",
|
||||
"jsdebug.tag.duration": "Duration: ",
|
||||
"jsdebug.tag.success": "Success",
|
||||
"jsdebug.tag.failed": "Failed",
|
||||
"jsdebug.btn.run": "Run",
|
||||
"jsdebug.btn.clear": "Clear",
|
||||
"jsdebug.section.code": "Code",
|
||||
"jsdebug.section.output": "Output",
|
||||
"jsdebug.btn.copy": "Copy",
|
||||
"jsdebug.section.return_value": "Return Value",
|
||||
"jsdebug.code_placeholder": "Enter JavaScript code",
|
||||
"jsdebug.output_placeholder": "console.log output will appear here",
|
||||
"jsdebug.return_placeholder": "Function return value will appear here",
|
||||
"jsdebug.msg.code_required": "Please enter code",
|
||||
"jsdebug.msg.execute_failed": "Execution failed: ",
|
||||
"jsdebug.msg.copy_prefix": "Return value: ",
|
||||
"jsdebug.msg.copied": "Copied to clipboard",
|
||||
"jsdebug.msg.copy_failed": "Copy failed",
|
||||
|
||||
"jsonparser.title": "JSON Parser",
|
||||
"jsonparser.stats.objects": "Objects: ",
|
||||
"jsonparser.stats.arrays": "Arrays: ",
|
||||
"jsonparser.stats.strings": "Strings: ",
|
||||
"jsonparser.stats.numbers": "Numbers: ",
|
||||
"jsonparser.view.tree": "Tree",
|
||||
"jsonparser.view.table": "Table",
|
||||
"jsonparser.view.raw": "Raw",
|
||||
"jsonparser.btn.copy": "Copy",
|
||||
"jsonparser.btn.clear": "Clear",
|
||||
"jsonparser.section.input": "Input",
|
||||
"jsonparser.section.result": "Result",
|
||||
"jsonparser.input_placeholder": "Paste or enter JSON content",
|
||||
"jsonparser.raw_placeholder": "Formatted JSON will appear here",
|
||||
"jsonparser.empty": "Enter JSON data to parse",
|
||||
"jsonparser.table.column.key": "Key",
|
||||
"jsonparser.table.column.index": "Index",
|
||||
"jsonparser.table.column.value": "Value",
|
||||
"jsonparser.table.column.action": "Action",
|
||||
"jsonparser.msg.parse_error": "JSON parse error: ",
|
||||
"jsonparser.msg.copied": "Copied to clipboard",
|
||||
"jsonparser.msg.copy_failed": "Copy failed",
|
||||
"jsonparser.msg.received": "JSON data received",
|
||||
|
||||
"socket.connection.title": "Connection",
|
||||
"socket.connection.status.connected": "Connected",
|
||||
"socket.connection.status.disconnected": "Disconnected",
|
||||
"socket.connection.btn.connect": "Connect",
|
||||
"socket.connection.btn.disconnect": "Disconnect",
|
||||
"socket.connection.label.protocol": "Protocol",
|
||||
"socket.connection.label.address": "Address",
|
||||
"socket.connection.label.port": "Port",
|
||||
"socket.connection.label.path": "Path",
|
||||
"socket.connection.label.headers": "Headers",
|
||||
"socket.connection.placeholder.host": "Host address",
|
||||
"socket.connection.placeholder.port": "8080",
|
||||
"socket.connection.placeholder.path": "/",
|
||||
"socket.connection.placeholder.headers": "Optional, format: Key: Value",
|
||||
"socket.message.title": "Messages",
|
||||
"socket.message.auto_scroll": "Auto Scroll",
|
||||
"socket.message.btn.clear": "Clear",
|
||||
"socket.message.tag.sent": "Sent",
|
||||
"socket.message.tag.received": "Received",
|
||||
"socket.message.tag.error": "Error",
|
||||
"socket.message.tag.system": "System",
|
||||
"socket.message.empty": "No messages",
|
||||
"socket.message.placeholder": "Enter message",
|
||||
"socket.message.btn.send": "Send",
|
||||
"socket.msg.host_required": "Please enter host address",
|
||||
"socket.msg.port_invalid": "Invalid port",
|
||||
"socket.msg.connect_failed": "Connection failed: ",
|
||||
"socket.msg.disconnect_failed": "Disconnect failed: ",
|
||||
"socket.msg.not_connected": "Not connected",
|
||||
"socket.msg.send_failed": "Send failed: ",
|
||||
|
||||
"settings.title": "Settings",
|
||||
"settings.form.language_label": "Language",
|
||||
"settings.form.proxy_divider": "Proxy",
|
||||
"settings.form.enable_proxy_label": "Enable Proxy",
|
||||
"settings.form.proxy_address_label": "Proxy Address",
|
||||
"settings.form.proxy_address_placeholder": "127.0.0.1",
|
||||
"settings.form.proxy_port_label": "Proxy Port",
|
||||
"settings.form.btn.save": "Save Settings",
|
||||
"settings.form.btn.reset": "Reset to Default",
|
||||
"settings.msg.saved": "Settings saved",
|
||||
"settings.msg.reset": "Settings reset"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"language_name": "日本語",
|
||||
"language_code": "ja-JP",
|
||||
"textmap_path": "textmap.json",
|
||||
"translation_progress": "100%",
|
||||
"translator": "MoGuQAQ",
|
||||
"last_updated": "2025-07-26",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
{
|
||||
"app.tab.webdebug": "ウェブデバッグ",
|
||||
"app.tab.jsdebug": "JSデバッグ",
|
||||
"app.tab.jsonparser": "JSONパーサー",
|
||||
"app.tab.socketdebug": "Socketデバッグ",
|
||||
"app.tab.settings": "設定",
|
||||
|
||||
"header.app_name": "WebHelper",
|
||||
|
||||
"webdebug.url_placeholder": "リクエストURLを入力",
|
||||
"webdebug.btn_send": "送信",
|
||||
"webdebug.tab.headers": "ヘッダー",
|
||||
"webdebug.tab.body": "ボディ",
|
||||
"webdebug.tab.cookie": "Cookie",
|
||||
"webdebug.tab.advanced": "詳細設定",
|
||||
"webdebug.header.key_placeholder": "キー",
|
||||
"webdebug.header.value_placeholder": "値",
|
||||
"webdebug.header.add": "ヘッダーを追加",
|
||||
"webdebug.body.type_text": "テキスト",
|
||||
"webdebug.body.type_json": "JSON",
|
||||
"webdebug.body.type_form": "フォーム",
|
||||
"webdebug.body.type_file": "ファイル",
|
||||
"webdebug.body.file_placeholder": "ファイルを選択",
|
||||
"webdebug.body.file_btn": "ファイルを選択",
|
||||
"webdebug.body.json_placeholder": "JSONデータを入力",
|
||||
"webdebug.body.text_placeholder": "リクエストボディを入力",
|
||||
"webdebug.body.cookie_placeholder": "Cookie文字列(形式: name1=value1; name2=value2)",
|
||||
"webdebug.advanced.proxy_label": "プロキシ",
|
||||
"webdebug.advanced.proxy_placeholder": "http://127.0.0.1:7890",
|
||||
"webdebug.advanced.timeout_label": "タイムアウト",
|
||||
"webdebug.advanced.timeout_unit": "秒",
|
||||
"webdebug.advanced.content_type_label": "Content-Type",
|
||||
"webdebug.advanced.content_type_placeholder": "自動検出",
|
||||
"webdebug.advanced.insecure_label": "証明書を無視",
|
||||
"webdebug.advanced.log_divider": "ログ設定",
|
||||
"webdebug.advanced.save_log_label": "ログを保存",
|
||||
"webdebug.advanced.log_manage_label": "ログ管理",
|
||||
"webdebug.advanced.view_logs_btn": "ログを表示",
|
||||
"webdebug.response.title": "レスポンス",
|
||||
"webdebug.response.tab.body": "本文",
|
||||
"webdebug.response.tab.headers": "ヘッダー",
|
||||
"webdebug.response.tab.more": "詳細情報",
|
||||
"webdebug.response.body_placeholder": "レスポンス本文がここに表示されます",
|
||||
"webdebug.response.send_to_json": "JSONパーサーに送信",
|
||||
"webdebug.response.headers_placeholder": "レスポンスヘッダーがここに表示されます",
|
||||
"webdebug.response.status_code_label": "ステータスコード",
|
||||
"webdebug.response.status_label": "ステータス",
|
||||
"webdebug.response.content_type_label": "Content-Type",
|
||||
"webdebug.response.content_length_label": "コンテンツ長",
|
||||
"webdebug.response.request_time_label": "所要時間",
|
||||
"webdebug.log.title": "リクエストログ",
|
||||
"webdebug.log.search_placeholder": "ログを検索...",
|
||||
"webdebug.log.clear": "クリア",
|
||||
"webdebug.log.empty": "ログなし",
|
||||
"webdebug.log.tab.request_info": "リクエスト情報",
|
||||
"webdebug.log.tab.response_info": "レスポンス情報",
|
||||
"webdebug.log.request.method_label": "メソッド",
|
||||
"webdebug.log.request.url_label": "URL",
|
||||
"webdebug.log.request.host_label": "ホスト",
|
||||
"webdebug.log.request.path_label": "パス",
|
||||
"webdebug.log.request.body_label": "ボディ",
|
||||
"webdebug.log.response.status_code_label": "ステータスコード",
|
||||
"webdebug.log.response.status_label": "ステータス",
|
||||
"webdebug.log.response.size_label": "サイズ",
|
||||
"webdebug.log.response.time_label": "所要時間",
|
||||
"webdebug.log.response.body_label": "レスポンス本文",
|
||||
"webdebug.log.load_to_debug": "デバッガーに読み込む",
|
||||
"webdebug.log.select_hint": "ログを選択して詳細を表示",
|
||||
"webdebug.msg.url_required": "URLを入力してください",
|
||||
"webdebug.msg.request_failed": "リクエスト失敗: ",
|
||||
"webdebug.msg.load_logs_failed": "ログの読み込みに失敗: ",
|
||||
"webdebug.msg.delete_failed": "削除に失敗: ",
|
||||
"webdebug.msg.clear_failed": "クリアに失敗: ",
|
||||
|
||||
"jsdebug.title": "JavaScriptデバッガー",
|
||||
"jsdebug.tag.duration": "所要時間: ",
|
||||
"jsdebug.tag.success": "成功",
|
||||
"jsdebug.tag.failed": "失敗",
|
||||
"jsdebug.btn.run": "実行",
|
||||
"jsdebug.btn.clear": "クリア",
|
||||
"jsdebug.section.code": "コード",
|
||||
"jsdebug.section.output": "出力",
|
||||
"jsdebug.btn.copy": "コピー",
|
||||
"jsdebug.section.return_value": "戻り値",
|
||||
"jsdebug.code_placeholder": "JavaScriptコードを入力",
|
||||
"jsdebug.output_placeholder": "console.log出力がここに表示されます",
|
||||
"jsdebug.return_placeholder": "関数の戻り値がここに表示されます",
|
||||
"jsdebug.msg.code_required": "コードを入力してください",
|
||||
"jsdebug.msg.execute_failed": "実行失敗: ",
|
||||
"jsdebug.msg.copy_prefix": "戻り値: ",
|
||||
"jsdebug.msg.copied": "クリップボードにコピーしました",
|
||||
"jsdebug.msg.copy_failed": "コピーに失敗しました",
|
||||
|
||||
"jsonparser.title": "JSONパーサー",
|
||||
"jsonparser.stats.objects": "オブジェクト: ",
|
||||
"jsonparser.stats.arrays": "配列: ",
|
||||
"jsonparser.stats.strings": "文字列: ",
|
||||
"jsonparser.stats.numbers": "数値: ",
|
||||
"jsonparser.view.tree": "ツリー",
|
||||
"jsonparser.view.table": "テーブル",
|
||||
"jsonparser.view.raw": "生データ",
|
||||
"jsonparser.btn.copy": "コピー",
|
||||
"jsonparser.btn.clear": "クリア",
|
||||
"jsonparser.section.input": "入力",
|
||||
"jsonparser.section.result": "解析結果",
|
||||
"jsonparser.input_placeholder": "JSONコンテンツを貼り付けまたは入力",
|
||||
"jsonparser.raw_placeholder": "フォーマットされたJSONがここに表示されます",
|
||||
"jsonparser.empty": "JSONデータを入力してください",
|
||||
"jsonparser.table.column.key": "キー",
|
||||
"jsonparser.table.column.index": "インデックス",
|
||||
"jsonparser.table.column.value": "値",
|
||||
"jsonparser.table.column.action": "操作",
|
||||
"jsonparser.msg.parse_error": "JSON解析エラー: ",
|
||||
"jsonparser.msg.copied": "クリップボードにコピーしました",
|
||||
"jsonparser.msg.copy_failed": "コピーに失敗しました",
|
||||
"jsonparser.msg.received": "JSONデータを受信しました",
|
||||
|
||||
"socket.connection.title": "接続設定",
|
||||
"socket.connection.status.connected": "接続済み",
|
||||
"socket.connection.status.disconnected": "未接続",
|
||||
"socket.connection.btn.connect": "接続",
|
||||
"socket.connection.btn.disconnect": "切断",
|
||||
"socket.connection.label.protocol": "プロトコル",
|
||||
"socket.connection.label.address": "アドレス",
|
||||
"socket.connection.label.port": "ポート",
|
||||
"socket.connection.label.path": "パス",
|
||||
"socket.connection.label.headers": "ヘッダー",
|
||||
"socket.connection.placeholder.host": "ホストアドレス",
|
||||
"socket.connection.placeholder.port": "8080",
|
||||
"socket.connection.placeholder.path": "/",
|
||||
"socket.connection.placeholder.headers": "オプション、形式: Key: Value",
|
||||
"socket.message.title": "メッセージ履歴",
|
||||
"socket.message.auto_scroll": "自動スクロール",
|
||||
"socket.message.btn.clear": "クリア",
|
||||
"socket.message.tag.sent": "送信",
|
||||
"socket.message.tag.received": "受信",
|
||||
"socket.message.tag.error": "エラー",
|
||||
"socket.message.tag.system": "システム",
|
||||
"socket.message.empty": "メッセージなし",
|
||||
"socket.message.placeholder": "メッセージを入力",
|
||||
"socket.message.btn.send": "送信",
|
||||
"socket.msg.host_required": "ホストアドレスを入力してください",
|
||||
"socket.msg.port_invalid": "ポート番号が無効です",
|
||||
"socket.msg.connect_failed": "接続失敗: ",
|
||||
"socket.msg.disconnect_failed": "切断失敗: ",
|
||||
"socket.msg.not_connected": "未接続です",
|
||||
"socket.msg.send_failed": "送信失敗: ",
|
||||
|
||||
"settings.title": "設定",
|
||||
"settings.form.language_label": "言語",
|
||||
"settings.form.proxy_divider": "プロキシ",
|
||||
"settings.form.enable_proxy_label": "プロキシを有効にする",
|
||||
"settings.form.proxy_address_label": "プロキシアドレス",
|
||||
"settings.form.proxy_address_placeholder": "127.0.0.1",
|
||||
"settings.form.proxy_port_label": "プロキシポート",
|
||||
"settings.form.btn.save": "設定を保存",
|
||||
"settings.form.btn.reset": "デフォルトに戻す",
|
||||
"settings.msg.saved": "設定を保存しました",
|
||||
"settings.msg.reset": "設定をリセットしました"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"language_name": "한국어",
|
||||
"language_code": "ko-KR",
|
||||
"textmap_path": "textmap.json",
|
||||
"translation_progress": "100%",
|
||||
"translator": "MoGuQAQ",
|
||||
"last_updated": "2025-07-26",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
{
|
||||
"app.tab.webdebug": "웹 디버그",
|
||||
"app.tab.jsdebug": "JS 디버그",
|
||||
"app.tab.jsonparser": "JSON 파서",
|
||||
"app.tab.socketdebug": "Socket 디버그",
|
||||
"app.tab.settings": "설정",
|
||||
|
||||
"header.app_name": "WebHelper",
|
||||
|
||||
"webdebug.url_placeholder": "요청 URL 입력",
|
||||
"webdebug.btn_send": "보내기",
|
||||
"webdebug.tab.headers": "헤더",
|
||||
"webdebug.tab.body": "본문",
|
||||
"webdebug.tab.cookie": "Cookie",
|
||||
"webdebug.tab.advanced": "고급 설정",
|
||||
"webdebug.header.key_placeholder": "키",
|
||||
"webdebug.header.value_placeholder": "값",
|
||||
"webdebug.header.add": "헤더 추가",
|
||||
"webdebug.body.type_text": "텍스트",
|
||||
"webdebug.body.type_json": "JSON",
|
||||
"webdebug.body.type_form": "폼",
|
||||
"webdebug.body.type_file": "파일",
|
||||
"webdebug.body.file_placeholder": "파일 선택",
|
||||
"webdebug.body.file_btn": "파일 선택",
|
||||
"webdebug.body.json_placeholder": "JSON 데이터 입력",
|
||||
"webdebug.body.text_placeholder": "요청 본문 입력",
|
||||
"webdebug.body.cookie_placeholder": "Cookie 문자열 (형식: name1=value1; name2=value2)",
|
||||
"webdebug.advanced.proxy_label": "프록시",
|
||||
"webdebug.advanced.proxy_placeholder": "http://127.0.0.1:7890",
|
||||
"webdebug.advanced.timeout_label": "타임아웃",
|
||||
"webdebug.advanced.timeout_unit": "초",
|
||||
"webdebug.advanced.content_type_label": "Content-Type",
|
||||
"webdebug.advanced.content_type_placeholder": "자동 감지",
|
||||
"webdebug.advanced.insecure_label": "인증서 무시",
|
||||
"webdebug.advanced.log_divider": "로그 설정",
|
||||
"webdebug.advanced.save_log_label": "로그 저장",
|
||||
"webdebug.advanced.log_manage_label": "로그 관리",
|
||||
"webdebug.advanced.view_logs_btn": "로그 보기",
|
||||
"webdebug.response.title": "응답",
|
||||
"webdebug.response.tab.body": "본문",
|
||||
"webdebug.response.tab.headers": "헤더",
|
||||
"webdebug.response.tab.more": "추가 정보",
|
||||
"webdebug.response.body_placeholder": "응답 본문이 여기에 표시됩니다",
|
||||
"webdebug.response.send_to_json": "JSON 파서로 보내기",
|
||||
"webdebug.response.headers_placeholder": "응답 헤더가 여기에 표시됩니다",
|
||||
"webdebug.response.status_code_label": "상태 코드",
|
||||
"webdebug.response.status_label": "상태",
|
||||
"webdebug.response.content_type_label": "Content-Type",
|
||||
"webdebug.response.content_length_label": "콘텐츠 길이",
|
||||
"webdebug.response.request_time_label": "소요 시간",
|
||||
"webdebug.log.title": "요청 로그",
|
||||
"webdebug.log.search_placeholder": "로그 검색...",
|
||||
"webdebug.log.clear": "지우기",
|
||||
"webdebug.log.empty": "로그 없음",
|
||||
"webdebug.log.tab.request_info": "요청 정보",
|
||||
"webdebug.log.tab.response_info": "응답 정보",
|
||||
"webdebug.log.request.method_label": "메서드",
|
||||
"webdebug.log.request.url_label": "URL",
|
||||
"webdebug.log.request.host_label": "호스트",
|
||||
"webdebug.log.request.path_label": "경로",
|
||||
"webdebug.log.request.body_label": "본문",
|
||||
"webdebug.log.response.status_code_label": "상태 코드",
|
||||
"webdebug.log.response.status_label": "상태",
|
||||
"webdebug.log.response.size_label": "크기",
|
||||
"webdebug.log.response.time_label": "소요 시간",
|
||||
"webdebug.log.response.body_label": "응답 본문",
|
||||
"webdebug.log.load_to_debug": "디버거에 로드",
|
||||
"webdebug.log.select_hint": "로그를 선택하여 상세 정보 보기",
|
||||
"webdebug.msg.url_required": "URL을 입력하세요",
|
||||
"webdebug.msg.request_failed": "요청 실패: ",
|
||||
"webdebug.msg.load_logs_failed": "로그 로드 실패: ",
|
||||
"webdebug.msg.delete_failed": "삭제 실패: ",
|
||||
"webdebug.msg.clear_failed": "지우기 실패: ",
|
||||
|
||||
"jsdebug.title": "JavaScript 디버거",
|
||||
"jsdebug.tag.duration": "소요 시간: ",
|
||||
"jsdebug.tag.success": "성공",
|
||||
"jsdebug.tag.failed": "실패",
|
||||
"jsdebug.btn.run": "실행",
|
||||
"jsdebug.btn.clear": "지우기",
|
||||
"jsdebug.section.code": "코드",
|
||||
"jsdebug.section.output": "출력",
|
||||
"jsdebug.btn.copy": "복사",
|
||||
"jsdebug.section.return_value": "반환값",
|
||||
"jsdebug.code_placeholder": "JavaScript 코드 입력",
|
||||
"jsdebug.output_placeholder": "console.log 출력이 여기에 표시됩니다",
|
||||
"jsdebug.return_placeholder": "함수 반환값이 여기에 표시됩니다",
|
||||
"jsdebug.msg.code_required": "코드를 입력하세요",
|
||||
"jsdebug.msg.execute_failed": "실행 실패: ",
|
||||
"jsdebug.msg.copy_prefix": "반환값: ",
|
||||
"jsdebug.msg.copied": "클립보드에 복사됨",
|
||||
"jsdebug.msg.copy_failed": "복사 실패",
|
||||
|
||||
"jsonparser.title": "JSON 파서",
|
||||
"jsonparser.stats.objects": "객체: ",
|
||||
"jsonparser.stats.arrays": "배열: ",
|
||||
"jsonparser.stats.strings": "문자열: ",
|
||||
"jsonparser.stats.numbers": "숫자: ",
|
||||
"jsonparser.view.tree": "트리",
|
||||
"jsonparser.view.table": "테이블",
|
||||
"jsonparser.view.raw": "원본",
|
||||
"jsonparser.btn.copy": "복사",
|
||||
"jsonparser.btn.clear": "지우기",
|
||||
"jsonparser.section.input": "입력",
|
||||
"jsonparser.section.result": "파싱 결과",
|
||||
"jsonparser.input_placeholder": "JSON 콘텐츠를 붙여넣거나 입력",
|
||||
"jsonparser.raw_placeholder": "포맷된 JSON이 여기에 표시됩니다",
|
||||
"jsonparser.empty": "JSON 데이터를 입력하세요",
|
||||
"jsonparser.table.column.key": "키",
|
||||
"jsonparser.table.column.index": "인덱스",
|
||||
"jsonparser.table.column.value": "값",
|
||||
"jsonparser.table.column.action": "작업",
|
||||
"jsonparser.msg.parse_error": "JSON 파싱 오류: ",
|
||||
"jsonparser.msg.copied": "클립보드에 복사됨",
|
||||
"jsonparser.msg.copy_failed": "복사 실패",
|
||||
"jsonparser.msg.received": "JSON 데이터 수신됨",
|
||||
|
||||
"socket.connection.title": "연결 설정",
|
||||
"socket.connection.status.connected": "연결됨",
|
||||
"socket.connection.status.disconnected": "미연결",
|
||||
"socket.connection.btn.connect": "연결",
|
||||
"socket.connection.btn.disconnect": "해제",
|
||||
"socket.connection.label.protocol": "프로토콜",
|
||||
"socket.connection.label.address": "주소",
|
||||
"socket.connection.label.port": "포트",
|
||||
"socket.connection.label.path": "경로",
|
||||
"socket.connection.label.headers": "헤더",
|
||||
"socket.connection.placeholder.host": "호스트 주소",
|
||||
"socket.connection.placeholder.port": "8080",
|
||||
"socket.connection.placeholder.path": "/",
|
||||
"socket.connection.placeholder.headers": "선택, 형식: Key: Value",
|
||||
"socket.message.title": "메시지 기록",
|
||||
"socket.message.auto_scroll": "자동 스크롤",
|
||||
"socket.message.btn.clear": "지우기",
|
||||
"socket.message.tag.sent": "보냄",
|
||||
"socket.message.tag.received": "수신",
|
||||
"socket.message.tag.error": "오류",
|
||||
"socket.message.tag.system": "시스템",
|
||||
"socket.message.empty": "메시지 없음",
|
||||
"socket.message.placeholder": "메시지 입력",
|
||||
"socket.message.btn.send": "보내기",
|
||||
"socket.msg.host_required": "호스트 주소를 입력하세요",
|
||||
"socket.msg.port_invalid": "잘못된 포트",
|
||||
"socket.msg.connect_failed": "연결 실패: ",
|
||||
"socket.msg.disconnect_failed": "해제 실패: ",
|
||||
"socket.msg.not_connected": "연결되지 않음",
|
||||
"socket.msg.send_failed": "전송 실패: ",
|
||||
|
||||
"settings.title": "설정",
|
||||
"settings.form.language_label": "언어",
|
||||
"settings.form.proxy_divider": "프록시",
|
||||
"settings.form.enable_proxy_label": "프록시 활성화",
|
||||
"settings.form.proxy_address_label": "프록시 주소",
|
||||
"settings.form.proxy_address_placeholder": "127.0.0.1",
|
||||
"settings.form.proxy_port_label": "프록시 포트",
|
||||
"settings.form.btn.save": "설정 저장",
|
||||
"settings.form.btn.reset": "기본값으로 복원",
|
||||
"settings.msg.saved": "설정이 저장되었습니다",
|
||||
"settings.msg.reset": "설정이 초기화되었습니다"
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var Log *logrus.Logger
|
||||
|
||||
// ANSI 颜色代码
|
||||
const (
|
||||
red = 31
|
||||
yellow = 33
|
||||
blue = 36
|
||||
gray = 37
|
||||
)
|
||||
|
||||
// LogFormatter 自定义日志格式化器
|
||||
type LogFormatter struct{}
|
||||
|
||||
func (f *LogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||
var levelColor int
|
||||
switch entry.Level {
|
||||
case logrus.DebugLevel, logrus.TraceLevel:
|
||||
levelColor = gray
|
||||
case logrus.WarnLevel:
|
||||
levelColor = yellow
|
||||
case logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel:
|
||||
levelColor = red
|
||||
default:
|
||||
levelColor = blue
|
||||
}
|
||||
|
||||
var b *bytes.Buffer
|
||||
if entry.Buffer != nil {
|
||||
b = entry.Buffer
|
||||
} else {
|
||||
b = &bytes.Buffer{}
|
||||
}
|
||||
|
||||
timestamp := entry.Time.Format("2006/01/02 15:04:05")
|
||||
if entry.HasCaller() {
|
||||
funcVal := entry.Caller.Function
|
||||
fileVal := fmt.Sprintf("%s:%d", path.Base(entry.Caller.File), entry.Caller.Line)
|
||||
fmt.Fprintf(b, "%s[%s] \033[%dm[%s]\033[0m %s %s: %s\n",
|
||||
"[Logger]", timestamp, levelColor, entry.Level, funcVal, fileVal, entry.Message)
|
||||
} else {
|
||||
fmt.Fprintf(b, "%s[%s] \033[%dm[%s]\033[0m %s\n",
|
||||
"[Logger]", timestamp, levelColor, entry.Level, entry.Message)
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
// FileFormatter 文件日志格式化器(不包含 ANSI 颜色代码)
|
||||
type FileFormatter struct{}
|
||||
|
||||
func (f *FileFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||
var b *bytes.Buffer
|
||||
if entry.Buffer != nil {
|
||||
b = entry.Buffer
|
||||
} else {
|
||||
b = &bytes.Buffer{}
|
||||
}
|
||||
|
||||
timestamp := entry.Time.Format("2006/01/02 15:04:05")
|
||||
if entry.HasCaller() {
|
||||
funcVal := entry.Caller.Function
|
||||
fileVal := fmt.Sprintf("%s:%d", path.Base(entry.Caller.File), entry.Caller.Line)
|
||||
fmt.Fprintf(b, "%s[%s] [%s] %s %s: %s\n",
|
||||
"[Logger]", timestamp, entry.Level, funcVal, fileVal, entry.Message)
|
||||
} else {
|
||||
fmt.Fprintf(b, "%s[%s] [%s] %s\n",
|
||||
"[Logger]", timestamp, entry.Level, entry.Message)
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
// InitLogger 初始化日志系统
|
||||
// 每次运行时按照启动时间保存日志文件,并自动删除最新十个日志之前的日志
|
||||
func InitLogger() {
|
||||
Log = logrus.New()
|
||||
Log.SetReportCaller(true)
|
||||
Log.SetFormatter(&LogFormatter{})
|
||||
Log.SetLevel(logrus.DebugLevel)
|
||||
|
||||
// 确保日志目录存在
|
||||
logDir := "logs"
|
||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||
Log.Warnf("无法创建日志目录: %v", err)
|
||||
}
|
||||
|
||||
// 清理旧日志(保留最新10个)
|
||||
cleanOldLogs(logDir, 10)
|
||||
|
||||
// 创建按启动时间命名的日志文件
|
||||
startTime := time.Now().Format("2006-01-02_15-04-05")
|
||||
logFile := filepath.Join(logDir, startTime+".log")
|
||||
file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
Log.Warnf("无法打开日志文件: %v", err)
|
||||
} else {
|
||||
// 控制台输出使用带颜色的格式
|
||||
Log.SetOutput(os.Stdout)
|
||||
|
||||
// 文件输出使用无颜色的格式,通过 Hook 实现
|
||||
Log.AddHook(&FileHook{file: file})
|
||||
}
|
||||
}
|
||||
|
||||
// FileHook 将日志同时写入文件的 Hook
|
||||
type FileHook struct {
|
||||
file *os.File
|
||||
}
|
||||
|
||||
func (h *FileHook) Levels() []logrus.Level {
|
||||
return logrus.AllLevels
|
||||
}
|
||||
|
||||
func (h *FileHook) Fire(entry *logrus.Entry) error {
|
||||
formatter := &FileFormatter{}
|
||||
line, err := formatter.Format(entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = h.file.Write(line)
|
||||
return err
|
||||
}
|
||||
|
||||
// cleanOldLogs 清理旧日志文件,保留最新的 maxKeep 个日志
|
||||
func cleanOldLogs(logDir string, maxKeep int) {
|
||||
entries, err := os.ReadDir(logDir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 收集所有 .log 文件
|
||||
var logFiles []os.DirEntry
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".log") {
|
||||
logFiles = append(logFiles, entry)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果日志文件数量未超过限制,无需清理
|
||||
if len(logFiles) <= maxKeep {
|
||||
return
|
||||
}
|
||||
|
||||
// 按文件名排序(文件名包含时间戳,排序即按时间排序)
|
||||
sort.Slice(logFiles, func(i, j int) bool {
|
||||
return logFiles[i].Name() < logFiles[j].Name()
|
||||
})
|
||||
|
||||
// 删除超出限制的旧日志
|
||||
filesToDelete := logFiles[:len(logFiles)-maxKeep]
|
||||
for _, file := range filesToDelete {
|
||||
filePath := filepath.Join(logDir, file.Name())
|
||||
os.Remove(filePath)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"webhelper/global"
|
||||
"webhelper/service"
|
||||
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
)
|
||||
|
||||
//go:embed all:frontend/dist
|
||||
var assets embed.FS
|
||||
|
||||
//go:embed lang/*
|
||||
var langFS embed.FS
|
||||
|
||||
func main() {
|
||||
global.LangFS = langFS
|
||||
global.Init()
|
||||
|
||||
appName := global.GetProcessName()
|
||||
App := service.NewApp()
|
||||
|
||||
err := wails.Run(&options.App{
|
||||
Title: appName,
|
||||
Width: 1024,
|
||||
Height: 768,
|
||||
MinWidth: 800,
|
||||
MinHeight: 600,
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
},
|
||||
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||
OnStartup: App.Startup,
|
||||
Frameless: true,
|
||||
Bind: []interface{}{
|
||||
App,
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
println("Error:", err.Error())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,517 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
goruntime "runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"webhelper/api"
|
||||
"webhelper/global"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
}
|
||||
|
||||
func (a *App) Startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
}
|
||||
|
||||
func (a *App) Greet(name string) string {
|
||||
return greet(name)
|
||||
}
|
||||
|
||||
func (a *App) Flashtime() {
|
||||
flashtime(a)
|
||||
}
|
||||
|
||||
func (a *App) Gettestjson() string {
|
||||
return getJSONString()
|
||||
}
|
||||
|
||||
func (a *App) GetLangTextMap() map[string]string {
|
||||
return global.GetLangTextMap()
|
||||
}
|
||||
|
||||
func (a *App) GetLangPack() *global.LanguagePack {
|
||||
langPack, err := global.GetLangPack()
|
||||
if err != nil {
|
||||
global.Log.Warnf("获取语言包失败: %v", err)
|
||||
return nil
|
||||
}
|
||||
return langPack
|
||||
}
|
||||
|
||||
func (a *App) GetALLLang() []global.LanguageInfo {
|
||||
return global.GetLangInfoList()
|
||||
}
|
||||
|
||||
func (a *App) SetLanguage(langCode string) bool {
|
||||
global.GlobalConfig.Language = langCode
|
||||
global.ClearLangCache()
|
||||
global.UpdateCurrentLangPath()
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *App) GetCurrentLang() string {
|
||||
return global.GlobalConfig.Language
|
||||
}
|
||||
|
||||
func (a *App) GetLogFiles() []string {
|
||||
logDir := global.GlobalConfig.LogDir
|
||||
entries, err := os.ReadDir(logDir)
|
||||
if err != nil {
|
||||
global.Log.Warnf("读取日志目录失败: %v", err)
|
||||
return []string{}
|
||||
}
|
||||
|
||||
var logFiles []string
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".log") {
|
||||
logFiles = append(logFiles, entry.Name())
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(sort.Reverse(sort.StringSlice(logFiles)))
|
||||
return logFiles
|
||||
}
|
||||
|
||||
func (a *App) GetLogFileContent(filename string) string {
|
||||
// 安全检查:防止路径遍历
|
||||
if strings.Contains(filename, "..") || strings.Contains(filename, "/") || strings.Contains(filename, "\\") {
|
||||
global.Log.Warnf("非法的日志文件名: %s", filename)
|
||||
return ""
|
||||
}
|
||||
|
||||
logPath := filepath.Join(global.GlobalConfig.LogDir, filename)
|
||||
data, err := os.ReadFile(logPath)
|
||||
if err != nil {
|
||||
global.Log.Warnf("读取日志文件失败: %v", err)
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func (a *App) SetLogLevel(level string) bool {
|
||||
switch strings.ToLower(level) {
|
||||
case "debug":
|
||||
global.SetLogLevel(logrus.DebugLevel)
|
||||
global.Log.Debug("日志等级已切换为 Debug")
|
||||
case "info":
|
||||
global.SetLogLevel(logrus.InfoLevel)
|
||||
global.Log.Info("日志等级已切换为 Info")
|
||||
case "warn":
|
||||
global.SetLogLevel(logrus.WarnLevel)
|
||||
global.Log.Warn("日志等级已切换为 Warn")
|
||||
case "error":
|
||||
global.SetLogLevel(logrus.ErrorLevel)
|
||||
global.Log.Error("日志等级已切换为 Error")
|
||||
default:
|
||||
global.Log.Warnf("未知的日志等级: %s", level)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *App) GetLogLevel() string {
|
||||
if global.Log == nil {
|
||||
return "info"
|
||||
}
|
||||
return strings.ToUpper(global.Log.GetLevel().String())
|
||||
}
|
||||
|
||||
func (a *App) WindowMinimise() {
|
||||
runtime.WindowMinimise(a.ctx)
|
||||
}
|
||||
|
||||
func (a *App) WindowToggleMaximise() {
|
||||
runtime.WindowToggleMaximise(a.ctx)
|
||||
}
|
||||
|
||||
func (a *App) WindowClose() {
|
||||
runtime.Quit(a.ctx)
|
||||
}
|
||||
|
||||
func (a *App) GetSystemInfo() SystemInfo {
|
||||
hostname, _ := os.Hostname()
|
||||
|
||||
return SystemInfo{
|
||||
OS: goruntime.GOOS,
|
||||
Arch: goruntime.GOARCH,
|
||||
NumCPU: goruntime.NumCPU(),
|
||||
Hostname: hostname,
|
||||
GoVer: goruntime.Version(),
|
||||
Time: time.Now().Format(time.DateTime),
|
||||
ProcessName: global.GetProcessName(),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) GetProcessName() string {
|
||||
return global.GetProcessName()
|
||||
}
|
||||
|
||||
func (a *App) GetVersion() string {
|
||||
return global.Version
|
||||
}
|
||||
|
||||
func (a *App) OpenFileSelect() string {
|
||||
file, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
|
||||
Title: "选择文件",
|
||||
Filters: []runtime.FileFilter{
|
||||
{DisplayName: "所有文件", Pattern: "*.*"},
|
||||
{DisplayName: "文本文件", Pattern: "*.txt"},
|
||||
{DisplayName: "JSON 文件", Pattern: "*.json"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
global.Log.Warnf("打开文件对话框失败: %v", err)
|
||||
return ""
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
func (a *App) OpenFolderSelect() string {
|
||||
folder, err := runtime.OpenDirectoryDialog(a.ctx, runtime.OpenDialogOptions{
|
||||
Title: "选择目录",
|
||||
})
|
||||
if err != nil {
|
||||
global.Log.Warnf("打开目录对话框失败: %v", err)
|
||||
return ""
|
||||
}
|
||||
return folder
|
||||
}
|
||||
|
||||
func (a *App) SaveFileSelect() string {
|
||||
file, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||
Title: "保存文件",
|
||||
Filters: []runtime.FileFilter{
|
||||
{DisplayName: "文本文件", Pattern: "*.txt"},
|
||||
{DisplayName: "JSON 文件", Pattern: "*.json"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
global.Log.Warnf("打开保存对话框失败: %v", err)
|
||||
return ""
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
func (a *App) ReadFileContent(path string) string {
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
global.Log.Warnf("读取文件失败: %v", err)
|
||||
return fmt.Sprintf("读取失败: %v", err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func (a *App) WriteFileContent(path string, content string) bool {
|
||||
if path == "" {
|
||||
return false
|
||||
}
|
||||
err := os.WriteFile(path, []byte(content), 0644)
|
||||
if err != nil {
|
||||
global.Log.Warnf("写入文件失败: %v", err)
|
||||
return false
|
||||
}
|
||||
global.Log.Infof("文件写入成功: %s", path)
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *App) Notify(title string, message string) {
|
||||
runtime.EventsEmit(a.ctx, "notification", map[string]string{
|
||||
"title": title,
|
||||
"message": message,
|
||||
})
|
||||
}
|
||||
|
||||
// SendHttpRequest 发送 HTTP 请求
|
||||
func (a *App) SendHttpRequest(method, url, headers, cookies, proxy, bodyType, body, filePath, contentType string, timeout int, insecure bool, saveLog bool) *api.HttpResponse {
|
||||
// 解析请求头
|
||||
headerMap := api.ParseHeaders(headers)
|
||||
|
||||
// 解析 Cookie
|
||||
cookieMap := api.ParseCookies(cookies)
|
||||
|
||||
req := &api.HttpRequest{
|
||||
Method: method,
|
||||
URL: url,
|
||||
Headers: headerMap,
|
||||
Cookies: cookieMap,
|
||||
Proxy: proxy,
|
||||
BodyType: bodyType,
|
||||
Body: body,
|
||||
FilePath: filePath,
|
||||
ContentType: contentType,
|
||||
Timeout: timeout,
|
||||
Insecure: insecure,
|
||||
}
|
||||
|
||||
global.Log.Infof("发送 HTTP 请求: %s %s", method, url)
|
||||
resp := api.SendRequest(req)
|
||||
|
||||
if resp.Error != "" {
|
||||
global.Log.Warnf("HTTP 请求失败: %s", resp.Error)
|
||||
} else {
|
||||
global.Log.Infof("HTTP 请求完成: %d %s (%v)", resp.StatusCode, resp.Status, resp.Time)
|
||||
}
|
||||
|
||||
// 保存日志
|
||||
if saveLog {
|
||||
logManager := api.GetLogManager()
|
||||
requestLog := api.CreateLogFromResponse(req, resp)
|
||||
if err := logManager.AddLog(requestLog); err != nil {
|
||||
global.Log.Warnf("保存请求日志失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// GetRequestLogs 获取请求日志列表
|
||||
func (a *App) GetRequestLogs() []*api.RequestLog {
|
||||
logManager := api.GetLogManager()
|
||||
return logManager.GetLogs()
|
||||
}
|
||||
|
||||
// GetRequestLog 获取单条请求日志
|
||||
func (a *App) GetRequestLog(id int) *api.RequestLog {
|
||||
logManager := api.GetLogManager()
|
||||
return logManager.GetLog(id)
|
||||
}
|
||||
|
||||
// DeleteRequestLog 删除请求日志
|
||||
func (a *App) DeleteRequestLog(id int) bool {
|
||||
logManager := api.GetLogManager()
|
||||
if err := logManager.DeleteLog(id); err != nil {
|
||||
global.Log.Warnf("删除请求日志失败: %v", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ClearRequestLogs 清空请求日志
|
||||
func (a *App) ClearRequestLogs() bool {
|
||||
logManager := api.GetLogManager()
|
||||
if err := logManager.ClearLogs(); err != nil {
|
||||
global.Log.Warnf("清空请求日志失败: %v", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// SearchRequestLogs 搜索请求日志
|
||||
func (a *App) SearchRequestLogs(keyword string) []*api.RequestLog {
|
||||
logManager := api.GetLogManager()
|
||||
return logManager.SearchLogs(keyword)
|
||||
}
|
||||
|
||||
// ExecuteJs 执行 JavaScript 代码
|
||||
func (a *App) ExecuteJs(code string) *api.JsExecResult {
|
||||
global.Log.Infof("执行 JavaScript 代码,长度: %d", len(code))
|
||||
result := api.ExecuteJs(code)
|
||||
|
||||
if result.Success {
|
||||
global.Log.Infof("JavaScript 执行成功,耗时: %s", result.Duration)
|
||||
} else {
|
||||
global.Log.Warnf("JavaScript 执行失败: %s", result.Error)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ConnectSocket 连接 WebSocket 或 TCP
|
||||
func (a *App) ConnectSocket(protocol, host string, port int, path string, headers string) bool {
|
||||
// 确保 path 以 / 开头
|
||||
if path == "" {
|
||||
path = "/"
|
||||
} else if path[0] != '/' {
|
||||
path = "/" + path
|
||||
}
|
||||
|
||||
var url string
|
||||
if protocol == "tcp" {
|
||||
url = fmt.Sprintf("%s:%d", host, port)
|
||||
} else {
|
||||
scheme := "ws"
|
||||
if protocol == "wss" {
|
||||
scheme = "wss"
|
||||
}
|
||||
url = fmt.Sprintf("%s://%s:%d%s", scheme, host, port, path)
|
||||
}
|
||||
|
||||
global.Log.Infof("连接 %s: %s", protocol, url)
|
||||
|
||||
// 发送连接中事件
|
||||
runtime.EventsEmit(a.ctx, "socket-status", map[string]string{
|
||||
"type": "connecting",
|
||||
"content": "正在连接...",
|
||||
})
|
||||
|
||||
if protocol == "tcp" {
|
||||
// TCP 连接
|
||||
config := &api.TCPConfig{
|
||||
Host: host,
|
||||
Port: port,
|
||||
}
|
||||
|
||||
err := api.ConnectTCP(config,
|
||||
// onMessage
|
||||
func(message string) {
|
||||
runtime.EventsEmit(a.ctx, "socket-message", map[string]string{
|
||||
"type": "received",
|
||||
"content": message,
|
||||
})
|
||||
},
|
||||
// onError
|
||||
func(err error) {
|
||||
// 连接失败时的错误回调
|
||||
runtime.EventsEmit(a.ctx, "socket-message", map[string]string{
|
||||
"type": "error",
|
||||
"content": err.Error(),
|
||||
})
|
||||
},
|
||||
// onClose
|
||||
func() {
|
||||
runtime.EventsEmit(a.ctx, "socket-status", map[string]string{
|
||||
"type": "disconnected",
|
||||
"content": "连接已断开",
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
global.Log.Warnf("TCP 连接失败: %v", err)
|
||||
runtime.EventsEmit(a.ctx, "socket-message", map[string]string{
|
||||
"type": "error",
|
||||
"content": "连接失败: " + err.Error(),
|
||||
})
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
// WebSocket 连接
|
||||
headerMap := api.ParseHeaders(headers)
|
||||
config := &api.WebSocketConfig{
|
||||
URL: url,
|
||||
Headers: headerMap,
|
||||
}
|
||||
|
||||
err := api.ConnectWebSocket(config,
|
||||
// onMessage
|
||||
func(message string) {
|
||||
runtime.EventsEmit(a.ctx, "socket-message", map[string]string{
|
||||
"type": "received",
|
||||
"content": message,
|
||||
})
|
||||
},
|
||||
// onError
|
||||
func(err error) {
|
||||
// 连接失败时的错误回调
|
||||
runtime.EventsEmit(a.ctx, "socket-message", map[string]string{
|
||||
"type": "error",
|
||||
"content": err.Error(),
|
||||
})
|
||||
},
|
||||
// onClose
|
||||
func() {
|
||||
runtime.EventsEmit(a.ctx, "socket-status", map[string]string{
|
||||
"type": "disconnected",
|
||||
"content": "连接已断开",
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
global.Log.Warnf("WebSocket 连接失败: %v", err)
|
||||
runtime.EventsEmit(a.ctx, "socket-message", map[string]string{
|
||||
"type": "error",
|
||||
"content": "连接失败: " + err.Error(),
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
global.Log.Infof("连接成功: %s", url)
|
||||
runtime.EventsEmit(a.ctx, "socket-status", map[string]string{
|
||||
"type": "connected",
|
||||
"content": "连接成功",
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// SendSocketMessage 发送 Socket 消息
|
||||
func (a *App) SendSocketMessage(protocol, message string) bool {
|
||||
global.Log.Infof("发送 %s 消息: %s", protocol, message)
|
||||
|
||||
var err error
|
||||
if protocol == "tcp" {
|
||||
err = api.SendTCPMessage(message)
|
||||
} else {
|
||||
err = api.SendWebSocketMessage(message)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
global.Log.Warnf("发送消息失败: %v", err)
|
||||
runtime.EventsEmit(a.ctx, "socket-message", map[string]string{
|
||||
"type": "error",
|
||||
"content": "发送失败: " + err.Error(),
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
// 发送成功事件
|
||||
runtime.EventsEmit(a.ctx, "socket-sent", map[string]string{
|
||||
"type": "sent",
|
||||
"content": message,
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// DisconnectSocket 断开 Socket 连接
|
||||
func (a *App) DisconnectSocket(protocol string) bool {
|
||||
global.Log.Infof("断开 %s 连接", protocol)
|
||||
|
||||
var err error
|
||||
if protocol == "tcp" {
|
||||
err = api.CloseTCP()
|
||||
} else {
|
||||
err = api.CloseWebSocket()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
global.Log.Warnf("断开连接失败: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
runtime.EventsEmit(a.ctx, "socket-status", map[string]string{
|
||||
"type": "disconnected",
|
||||
"content": "已断开连接",
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// IsSocketConnected 检查 Socket 是否已连接
|
||||
func (a *App) IsSocketConnected(protocol string) bool {
|
||||
if protocol == "tcp" {
|
||||
return api.IsTCPConnected()
|
||||
}
|
||||
return api.IsWebSocketConnected()
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
func greet(name string) string {
|
||||
return fmt.Sprintf("Hello %s, It's show time!", name)
|
||||
}
|
||||
|
||||
func flashtime(a *App) {
|
||||
go func() {
|
||||
for {
|
||||
now := time.Now().Format(time.DateTime)
|
||||
runtime.EventsEmit(a.ctx, "time", now)
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func getJSONString() string {
|
||||
datas := map[string]interface{}{
|
||||
"code": 200,
|
||||
"message": "ok",
|
||||
"data": []map[string]interface{}{
|
||||
{
|
||||
"id": 1,
|
||||
"name": "apps",
|
||||
"isdir": true,
|
||||
"size": 0,
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "docs",
|
||||
"isdir": true,
|
||||
"size": 0,
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "readme.txt",
|
||||
"isdir": false,
|
||||
"size": 2048,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(datas)
|
||||
if err != nil {
|
||||
return "{}"
|
||||
}
|
||||
|
||||
return string(jsonBytes)
|
||||
}
|
||||
|
||||
type SystemInfo struct {
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
NumCPU int `json:"num_cpu"`
|
||||
Hostname string `json:"hostname"`
|
||||
GoVer string `json:"go_ver"`
|
||||
Time string `json:"time"`
|
||||
ProcessName string `json:"process_name"`
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||
"name": "webhelper",
|
||||
"outputfilename": "webhelper",
|
||||
"frontend:install": "npm install",
|
||||
"frontend:build": "npm run build",
|
||||
"frontend:dev:watcher": "npm run dev",
|
||||
"frontend:dev:serverUrl": "auto",
|
||||
"author": {
|
||||
"name": "MoGuQAQ"
|
||||
},
|
||||
"info": {
|
||||
"companyName": "MoGuQAQ",
|
||||
"productName": "WebHelper",
|
||||
"productVersion": "1.0.0",
|
||||
"copyright": "Copyright © 2026 MoGuQAQ",
|
||||
"comments": "MIT license"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user