This commit is contained in:
mogumc committed 2026-05-27 19:18:10 +08:00
1 parent 7e430424fe
commit 2a6ec64e67
65 files changed
+10424

No files matched your search

+201
View File
@@ -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
View File
@@ -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/
+101
View File
@@ -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
View File
@@ -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
}
+176
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+21
View File
@@ -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
View File
@@ -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
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

+68
View File
@@ -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>
+63
View File
@@ -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

+15
View File
@@ -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}}"
}
}
}
+114
View File
@@ -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
+249
View File
@@ -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
+15
View File
@@ -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>
+13
View File
@@ -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>
+1705
View File
File diff suppressed because it is too large. Load diff
+22
View File
@@ -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"
}
}
+1
View File
@@ -0,0 +1 @@
4e89b15208d82473f2aacb5ee7a39659
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

+102
View File
@@ -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>
+93
View File
@@ -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.
+125
View File
@@ -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>
+188
View File
@@ -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>
+526
View File
@@ -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>
+52
View File
@@ -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>
+122
View File
@@ -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>
+462
View File
@@ -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>
+751
View File
@@ -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>
+771
View File
@@ -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>
+37
View File
@@ -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
}
}
+11
View File
@@ -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';
+31
View File
@@ -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;
}
+20
View File
@@ -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()],
}),
],
})
+227
View File
@@ -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"];
}
}
}
+73
View File
@@ -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>;
+139
View File
@@ -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);
}
+24
View File
@@ -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
View File
@@ -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>;
+298
View File
@@ -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);
}
+7
View File
@@ -0,0 +1,7 @@
//go:build debug
package global
// IsDebug 标记是否为调试模式
// 仅在使用 -tags debug 编译时为 true
var IsDebug = true
+7
View File
@@ -0,0 +1,7 @@
//go:build !debug
package global
// IsDebug 标记是否为调试模式
// 编译后为 false,不允许使用 debug 模式
var IsDebug = false
+11
View File
@@ -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
View File
@@ -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
View File
@@ -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"
}
}
+190
View File
@@ -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)
}
+6
View File
@@ -0,0 +1,6 @@
package global
// Version 版本号,通过 ldflags 注入
// 编译时: wails build -ldflags "-X webhelper/global.Version=v1.2.3"
// 开发时默认为 "dev"
var Version = "dev"
+45
View File
@@ -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
+104
View File
@@ -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=
+9
View File
@@ -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"
}
+160
View File
@@ -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": "设置已重置"
}
+9
View File
@@ -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"
}
+160
View File
@@ -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"
}
+9
View File
@@ -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"
}
+160
View File
@@ -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": "設定をリセットしました"
}
+9
View File
@@ -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"
}
+160
View File
@@ -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": "설정이 초기화되었습니다"
}
+168
View File
@@ -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)
}
}
+46
View File
@@ -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())
}
}
+517
View File
@@ -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()
}
+67
View File
@@ -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
View File
@@ -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"
}
}