This commit is contained in:
yml2213
2026-10-25 15:02:41 +08:00
commit ccd51b91fb
32 changed files with 5811 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
frontend/node_modules/
frontend/dist/
build/bin/
.DS_Store
*.log
+16
View File
@@ -0,0 +1,16 @@
.PHONY: dev build win test
VERSION := $(shell node -p "require('./wails.json').info.productVersion")
dev:
wails dev
build:
wails build
win:
wails build -platform windows/amd64 -o hfb_price_tool-v$(VERSION).exe
test:
go test ./...
cd frontend && npm run build
+155
View File
@@ -0,0 +1,155 @@
# 哈夫币发布计算器
一个独立的 Go + Wails 桌面工具,只计算**号主到手价**。输入保险格数、账号状态、哈弗币数量和物品数量后立即出结果。
界面按「保险格数 + 特殊状态 + 哈弗币 + 物品清单 + 底部分结果」布局,规则支持本地配置和从线上同步。
## 计算口径
```text
参考比例 = 保险基础比例
+ 缺配惩罚(红皮/刀皮/砖皮/满体力/满负重,每缺一项 +1)
+ 大额币修正(>130M +1;>230M +2.5;>380M +4;>530M +5)
+ 每日损耗修正(10/20/30/40/50M → +0/+1/+2/+3/+4)
最终比例 = clamp(加速比例, 参考比例, 参考比例 + 加速上限)
纯币到手 = 哈弗币万数 ÷ 最终比例
物品到手 = Σ(数量 × 单价) // 免费项不计
合计到手 = 纯币到手 + 物品到手
```
注意:比例数字越大,号主到手越低。加速出售是主动把比例调高、降低价格。
默认规则与 `hfb_sys` 主站发布口径一致:
- 保险基础比例:`3*3=40`、`2*3=42`、`2*2=47`、`2*1=48`
- 缺配项:刀皮、角色红皮、满体力、满负重、武器皮肤
- 大额修正:130M / 230M / 380M / 530M 四档
- 每日损耗:10M 到 50M
- 物品单价:AWM 子弹、6 级子弹、6 级头盔、6 级护甲、巴雷特子弹、顶级保险卡、咖啡豆、子弹零件、45 格大红包
`3*3` 保险下「顶级保险卡 / 9 格体验卡」按主站规则禁用。
## 本地运行
```bash
wails dev
```
Wails 程序不能用 `go run .` 启动;直接运行会缺少 Wails 需要的构建标签。
## 构建
macOS:
```bash
wails build
```
Windows exe(在 macOS 上可直接交叉编译,文件名自动带版本号):
```bash
wails build -platform windows/amd64 -o hfb_price_tool-v1.0.1.exe
```
> 不要加 `-nopackage`。加上它会跳过 Windows 资源打包,导致 exe 没有自定义图标、版本属性和应用清单。
Windows 安装包:
```bash
wails build -platform windows/amd64 -nsis
```
生成物在 `build/bin/`。最终 Windows 文件名示例:`hfb_price_tool-v1.0.1.exe`。
图标文件:
- `build/appicon.png`:macOS / 通用图标源图
- `build/windows/icon.ico`:Windows exe 图标(包含 16/24/32/48/64/128/256 多尺寸)
- `build/windows/info.json`:Windows 版本属性
- `build/windows/wails.exe.manifest`:Windows 应用清单
## 规则配置
右上角「配置」可以编辑:
- 基础比例
- 缺配惩罚
- 大额修正
- 物品单价 / 单位 / 停用
- 每日损耗选项、加速上限
本地配置保存在:
- Windows:`%AppData%\hfb-price-tool\config.json`
- macOS:`~/Library/Application Support/hfb-price-tool/config.json`
配置弹窗支持导入、导出和打开配置目录。首次启动会自动写入一份默认规则。
## 线上同步
「线上同步」调用主站公开接口:
```text
GET {地址}/api/listing-publish-options
```
默认地址:`https://hao858.com`
同步只覆盖:
- `ratio_config.insurance_base_ratios`
- `ratio_config.config_items`
- `ratio_config.coin_corrections`
- `quantity_items`
每日损耗选项、加速上限、本地物品停用状态会保留。同步成功后自动保存到本地配置。
接口响应结构:
```json
{
"code": "ok",
"message": "ok",
"data": {
"ratio_config": {},
"quantity_items": []
}
}
```
## 验证
```bash
go test ./...
cd frontend && npm run build
```
关键用例:`226M + 9格 + 5 项特殊状态全未勾选 + 每日损耗 10M`
```text
比例 = 40 + 5 + 1 = 46
226M = 22600 万
22600 ÷ 46 = 491.304... → ¥491.30
```
该用例固定在 `internal/pricing/calc_test.go`。
## 版本号
版本号的唯一来源是 `wails.json` 的 `info.productVersion`:
- Windows exe 的版本资源由它生成
- 界面右上角的版本号由 `frontend/vite.config.js` 在构建时读取同一字段注入
升级版本时只改这一处,然后重新构建。
## 目录
```text
app.go Wails 绑定、本地配置、线上同步、导入导出
main.go 窗口入口
internal/pricing/ 定价核心与单测
frontend/src/main.js 界面与交互
frontend/src/style.css 样式
```
+447
View File
@@ -0,0 +1,447 @@
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
goruntime "runtime"
"strings"
"sync"
"time"
"hfb_price_tool/internal/pricing"
wailsruntime "github.com/wailsapp/wails/v2/pkg/runtime"
)
const (
appName = "hfb-price-tool"
appVersion = "1.0.1"
publishEndpoint = "/api/listing-publish-options"
)
type App struct {
ctx context.Context
mu sync.RWMutex
config pricing.Config
configPath string
loadError string
}
type Bootstrap struct {
Config pricing.Config `json:"config"`
ConfigPath string `json:"config_path"`
ServerURL string `json:"server_url"`
LoadError string `json:"load_error,omitempty"`
}
type SyncResult struct {
Config pricing.Config `json:"config"`
ServerURL string `json:"server_url"`
SyncedAt string `json:"synced_at"`
InsuranceCount int `json:"insurance_count"`
RatioItemCount int `json:"ratio_item_count"`
CoinCorrectionCount int `json:"coin_correction_count"`
QuantityItemCount int `json:"quantity_item_count"`
}
type ExportResult struct {
Path string `json:"path"`
Cancelled bool `json:"cancelled"`
}
type ImportResult struct {
Config pricing.Config `json:"config"`
Path string `json:"path"`
Cancelled bool `json:"cancelled"`
}
type remotePublishOptions struct {
RatioConfig struct {
InsuranceBaseRatios []pricing.InsuranceBaseRatio `json:"insurance_base_ratios"`
ConfigItems []pricing.RatioConfigItem `json:"config_items"`
CoinCorrections []pricing.CoinCorrection `json:"coin_corrections"`
} `json:"ratio_config"`
QuantityItems []pricing.QuantityItem `json:"quantity_items"`
}
func NewApp() *App {
return &App{
config: pricing.DefaultConfig(),
}
}
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
path, err := defaultConfigPath()
if err != nil {
a.mu.Lock()
a.loadError = "无法读取本地配置目录:" + err.Error()
a.mu.Unlock()
return
}
a.mu.Lock()
a.configPath = path
a.mu.Unlock()
config, loadErr := loadConfigFile(path)
a.mu.Lock()
a.config = config
a.loadError = loadErr
a.mu.Unlock()
}
// Bootstrap 返回启动所需的本地规则、配置路径和默认线上地址。
func (a *App) Bootstrap() Bootstrap {
a.mu.RLock()
defer a.mu.RUnlock()
return Bootstrap{
Config: a.config,
ConfigPath: a.configPath,
ServerURL: a.config.SyncURL,
LoadError: a.loadError,
}
}
// Calculate 按当前规则计算号主到手价。
func (a *App) Calculate(input pricing.Input) pricing.Result {
a.mu.RLock()
config := a.config
a.mu.RUnlock()
return config.Calculate(input)
}
// SaveConfig 校验并保存本地规则。
func (a *App) SaveConfig(config pricing.Config) (pricing.Config, error) {
normalized := pricing.Normalize(config)
if err := a.persist(normalized); err != nil {
return pricing.Config{}, err
}
return normalized, nil
}
// ResetConfig 恢复内置默认规则并落盘。
func (a *App) ResetConfig() (pricing.Config, error) {
config := pricing.DefaultConfig()
if err := a.persist(config); err != nil {
return pricing.Config{}, err
}
return config, nil
}
// SyncFromServer 从主站公开接口拉取发布规则,覆盖保险、缺配、大额修正和物品。
func (a *App) SyncFromServer(rawURL string) (SyncResult, error) {
baseURL, err := normalizeServerURL(rawURL)
if err != nil {
return SyncResult{}, err
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
remote, err := fetchRemotePublishOptions(ctx, baseURL)
if err != nil {
return SyncResult{}, err
}
if len(remote.RatioConfig.InsuranceBaseRatios) == 0 || len(remote.RatioConfig.ConfigItems) == 0 {
return SyncResult{}, errors.New("线上未返回可用的比例规则,请确认地址是否为哈夫币站点")
}
a.mu.RLock()
current := a.config
a.mu.RUnlock()
next := current
next.InsuranceBaseRatios = remote.RatioConfig.InsuranceBaseRatios
next.RatioConfigItems = remote.RatioConfig.ConfigItems
next.CoinCorrections = remote.RatioConfig.CoinCorrections
next.QuantityItems = mergeRemoteQuantityItems(current.QuantityItems, remote.QuantityItems)
next.SyncURL = baseURL
next = pricing.Normalize(next)
if err := a.persist(next); err != nil {
return SyncResult{}, err
}
return SyncResult{
Config: next,
ServerURL: baseURL,
SyncedAt: time.Now().Format("2006-01-02 15:04:05"),
InsuranceCount: len(next.InsuranceBaseRatios),
RatioItemCount: len(next.RatioConfigItems),
CoinCorrectionCount: len(next.CoinCorrections),
QuantityItemCount: len(next.QuantityItems),
}, nil
}
// ExportConfig 将当前规则导出为 JSON 文件。
func (a *App) ExportConfig() (ExportResult, error) {
a.mu.RLock()
config := a.config
a.mu.RUnlock()
path, err := wailsruntime.SaveFileDialog(a.ctx, wailsruntime.SaveDialogOptions{
Title: "导出规则配置",
DefaultFilename: "hfb-price-config.json",
Filters: []wailsruntime.FileFilter{
{DisplayName: "JSON 文件 (*.json)", Pattern: "*.json"},
},
})
if err != nil {
return ExportResult{}, err
}
if strings.TrimSpace(path) == "" {
return ExportResult{Cancelled: true}, nil
}
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
return ExportResult{}, fmt.Errorf("生成配置失败:%w", err)
}
data = append(data, '\n')
if err := os.WriteFile(path, data, 0o644); err != nil {
return ExportResult{}, fmt.Errorf("写入配置失败:%w", err)
}
return ExportResult{Path: path}, nil
}
// ImportConfig 从 JSON 文件导入规则。
func (a *App) ImportConfig() (ImportResult, error) {
path, err := wailsruntime.OpenFileDialog(a.ctx, wailsruntime.OpenDialogOptions{
Title: "导入规则配置",
Filters: []wailsruntime.FileFilter{
{DisplayName: "JSON 文件 (*.json)", Pattern: "*.json"},
},
})
if err != nil {
return ImportResult{}, err
}
if strings.TrimSpace(path) == "" {
return ImportResult{Cancelled: true}, nil
}
data, err := os.ReadFile(path)
if err != nil {
return ImportResult{}, fmt.Errorf("读取配置失败:%w", err)
}
var config pricing.Config
if err := json.Unmarshal(data, &config); err != nil {
return ImportResult{}, fmt.Errorf("配置格式不正确:%w", err)
}
config = pricing.Normalize(config)
if err := a.persist(config); err != nil {
return ImportResult{}, err
}
return ImportResult{Config: config, Path: path}, nil
}
// OpenConfigFolder 打开本地配置文件所在目录。
func (a *App) OpenConfigFolder() error {
a.mu.RLock()
path := a.configPath
a.mu.RUnlock()
if path == "" {
return errors.New("配置路径尚未初始化")
}
dir := filepath.Dir(path)
var command *exec.Cmd
switch goruntime.GOOS {
case "windows":
command = exec.Command("explorer", dir)
case "darwin":
command = exec.Command("open", dir)
default:
command = exec.Command("xdg-open", dir)
}
if err := command.Start(); err != nil {
return fmt.Errorf("打开目录失败:%w", err)
}
return nil
}
func (a *App) persist(config pricing.Config) error {
config = pricing.Normalize(config)
a.mu.RLock()
path := a.configPath
a.mu.RUnlock()
if path == "" {
defaultPath, err := defaultConfigPath()
if err != nil {
return fmt.Errorf("无法确定配置目录:%w", err)
}
path = defaultPath
a.mu.Lock()
a.configPath = path
a.mu.Unlock()
}
if err := writeConfigFile(path, config); err != nil {
return fmt.Errorf("保存规则失败:%w", err)
}
a.mu.Lock()
a.config = config
a.loadError = ""
a.mu.Unlock()
return nil
}
func defaultConfigPath() (string, error) {
dir, err := os.UserConfigDir()
if err != nil {
return "", err
}
return filepath.Join(dir, appName, "config.json"), nil
}
func loadConfigFile(path string) (pricing.Config, string) {
data, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
config := pricing.DefaultConfig()
if writeErr := writeConfigFile(path, config); writeErr != nil {
return config, "默认规则已加载,但写入本地配置失败:" + writeErr.Error()
}
return config, ""
}
if err != nil {
return pricing.DefaultConfig(), "读取本地配置失败,已使用默认规则:" + err.Error()
}
var config pricing.Config
if err := json.Unmarshal(data, &config); err != nil {
return pricing.DefaultConfig(), "本地配置格式不正确,已使用默认规则:" + err.Error()
}
return pricing.Normalize(config), ""
}
func writeConfigFile(path string, config pricing.Config) error {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
return err
}
data = append(data, '\n')
temp, err := os.CreateTemp(filepath.Dir(path), ".config-*.json")
if err != nil {
return err
}
tempName := temp.Name()
defer os.Remove(tempName)
if _, err := temp.Write(data); err != nil {
temp.Close()
return err
}
if err := temp.Close(); err != nil {
return err
}
if err := os.Chmod(tempName, 0o644); err != nil {
return err
}
return os.Rename(tempName, path)
}
func normalizeServerURL(raw string) (string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", errors.New("请填写线上地址")
}
if !strings.Contains(raw, "://") {
raw = "https://" + raw
}
parsed, err := url.Parse(raw)
if err != nil || parsed.Host == "" {
return "", errors.New("线上地址格式不正确")
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return "", errors.New("线上地址只支持 http 或 https")
}
parsed.Path = strings.TrimRight(parsed.Path, "/")
parsed.RawQuery = ""
parsed.Fragment = ""
return strings.TrimRight(parsed.String(), "/"), nil
}
func fetchRemotePublishOptions(ctx context.Context, baseURL string) (remotePublishOptions, error) {
endpoint := baseURL + publishEndpoint
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return remotePublishOptions{}, fmt.Errorf("创建请求失败:%w", err)
}
request.Header.Set("Accept", "application/json")
request.Header.Set("User-Agent", appName+"/"+appVersion)
client := &http.Client{Timeout: 15 * time.Second}
response, err := client.Do(request)
if err != nil {
return remotePublishOptions{}, fmt.Errorf("连接线上失败:%w", err)
}
defer response.Body.Close()
body, err := io.ReadAll(io.LimitReader(response.Body, 4<<20))
if err != nil {
return remotePublishOptions{}, fmt.Errorf("读取线上响应失败:%w", err)
}
if response.StatusCode != http.StatusOK {
return remotePublishOptions{}, fmt.Errorf("线上返回 HTTP %d:%s", response.StatusCode, truncateText(string(body), 160))
}
var envelope struct {
Code string `json:"code"`
Message string `json:"message"`
Data json.RawMessage `json:"data"`
}
if err := json.Unmarshal(body, &envelope); err != nil {
return remotePublishOptions{}, fmt.Errorf("线上响应不是有效 JSON:%w", err)
}
if envelope.Code != "ok" {
message := strings.TrimSpace(envelope.Message)
if message == "" {
message = envelope.Code
}
return remotePublishOptions{}, fmt.Errorf("线上返回错误:%s", message)
}
var remote remotePublishOptions
if err := json.Unmarshal(envelope.Data, &remote); err != nil {
return remotePublishOptions{}, fmt.Errorf("解析线上规则失败:%w", err)
}
return remote, nil
}
func mergeRemoteQuantityItems(current, remote []pricing.QuantityItem) []pricing.QuantityItem {
currentByKey := make(map[string]pricing.QuantityItem, len(current))
for _, item := range current {
currentByKey[item.Key] = item
}
merged := make([]pricing.QuantityItem, 0, len(remote))
for _, item := range remote {
if old, ok := currentByKey[item.Key]; ok {
if strings.TrimSpace(item.Unit) == "" {
item.Unit = old.Unit
}
if strings.TrimSpace(item.Placeholder) == "" {
item.Placeholder = old.Placeholder
}
item.Disabled = old.Disabled
}
merged = append(merged, item)
}
if len(merged) == 0 {
return current
}
return merged
}
func truncateText(value string, limit int) string {
value = strings.TrimSpace(value)
if len(value) <= limit {
return value
}
return value[:limit] + "..."
}
+123
View File
@@ -0,0 +1,123 @@
package main
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"hfb_price_tool/internal/pricing"
)
func TestNormalizeServerURL(t *testing.T) {
cases := []struct {
raw string
want string
}{
{"https://hao858.com/", "https://hao858.com"},
{"hao858.com", "https://hao858.com"},
{"http://127.0.0.1:8080/", "http://127.0.0.1:8080"},
}
for _, item := range cases {
got, err := normalizeServerURL(item.raw)
if err != nil {
t.Fatalf("normalizeServerURL(%q) error: %v", item.raw, err)
}
if got != item.want {
t.Fatalf("normalizeServerURL(%q)=%q, want %q", item.raw, got, item.want)
}
}
if _, err := normalizeServerURL("ftp://hao858.com"); err == nil {
t.Fatal("ftp url should be rejected")
}
}
func TestFetchRemotePublishOptions(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if request.URL.Path != publishEndpoint {
t.Fatalf("unexpected path: %s", request.URL.Path)
}
writer.Header().Set("Content-Type", "application/json")
_, _ = writer.Write([]byte(`{
"code":"ok",
"message":"ok",
"data":{
"ratio_config":{
"insurance_base_ratios":[{"insurance":"3*3","ratio":40}],
"config_items":[{"key":"melee","label":"刀皮","kind":"skin_group","missing_penalty":1}],
"coin_corrections":[{"threshold_m":130,"correction":1}]
},
"quantity_items":[{"key":"awmAmmo","label":"AWM子弹","price":"0.6元/发"}]
}
}`))
}))
defer server.Close()
remote, err := fetchRemotePublishOptions(context.Background(), server.URL)
if err != nil {
t.Fatalf("fetchRemotePublishOptions error: %v", err)
}
if len(remote.RatioConfig.InsuranceBaseRatios) != 1 {
t.Fatalf("insurance count = %d, want 1", len(remote.RatioConfig.InsuranceBaseRatios))
}
if len(remote.QuantityItems) != 1 || remote.QuantityItems[0].Key != "awmAmmo" {
t.Fatalf("unexpected quantity items: %+v", remote.QuantityItems)
}
}
func TestFetchRemotePublishOptionsRejectsErrorEnvelope(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Type", "application/json")
_, _ = writer.Write([]byte(`{"code":"internal_error","message":"数据库连接失败"}`))
}))
defer server.Close()
if _, err := fetchRemotePublishOptions(context.Background(), server.URL); err == nil {
t.Fatal("expected error envelope to fail")
}
}
func TestMergeRemoteQuantityItemsPreservesLocalFlags(t *testing.T) {
current := []pricing.QuantityItem{
{Key: "awmAmmo", Unit: "发", Disabled: true, Placeholder: "本地提示"},
}
remote := []pricing.QuantityItem{
{Key: "awmAmmo", Label: "AWM子弹", Price: "0.8元/发"},
}
merged := mergeRemoteQuantityItems(current, remote)
if len(merged) != 1 {
t.Fatalf("merged count = %d, want 1", len(merged))
}
if merged[0].Unit != "发" || !merged[0].Disabled || merged[0].Placeholder != "本地提示" {
t.Fatalf("local flags were not preserved: %+v", merged[0])
}
}
func TestConfigFileRoundTrip(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.json")
config := pricing.DefaultConfig()
config.SyncURL = "https://example.com"
if err := writeConfigFile(path, config); err != nil {
t.Fatalf("writeConfigFile error: %v", err)
}
loaded, loadErr := loadConfigFile(path)
if loadErr != "" {
t.Fatalf("loadConfigFile error: %s", loadErr)
}
if loaded.SyncURL != "https://example.com" {
t.Fatalf("sync url = %q, want https://example.com", loaded.SyncURL)
}
if len(loaded.InsuranceBaseRatios) != 4 {
t.Fatalf("insurance count = %d, want 4", len(loaded.InsuranceBaseRatios))
}
raw, err := json.Marshal(loaded)
if err != nil {
t.Fatalf("marshal config: %v", err)
}
if len(raw) == 0 {
t.Fatal("marshaled config is empty")
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

+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.{{safeBundleID .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: 45 KiB

+16
View File
@@ -0,0 +1,16 @@
{
"fixed": {
"file_version": "{{.Info.ProductVersion}}",
"product_version": "{{.Info.ProductVersion}}"
},
"info": {
"0000": {
"ProductVersion": "{{.Info.ProductVersion}}",
"CompanyName": "{{.Info.CompanyName}}",
"FileDescription": "{{.Info.ProductName}}",
"LegalCopyright": "{{.Info.Copyright}}",
"ProductName": "{{.Info.ProductName}}",
"Comments": "{{.Info.Comments}}"
}
}
}
+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>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness>
</asmv3:windowsSettings>
</asmv3:application>
</assembly>
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>哈夫币发布计算器</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+664
View File
@@ -0,0 +1,664 @@
{
"name": "hfb-price-tool-frontend",
"version": "1.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hfb-price-tool-frontend",
"version": "1.0.1",
"devDependencies": {
"vite": "^3.0.7"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz",
"integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz",
"integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz",
"integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/android-arm": "0.15.18",
"@esbuild/linux-loong64": "0.15.18",
"esbuild-android-64": "0.15.18",
"esbuild-android-arm64": "0.15.18",
"esbuild-darwin-64": "0.15.18",
"esbuild-darwin-arm64": "0.15.18",
"esbuild-freebsd-64": "0.15.18",
"esbuild-freebsd-arm64": "0.15.18",
"esbuild-linux-32": "0.15.18",
"esbuild-linux-64": "0.15.18",
"esbuild-linux-arm": "0.15.18",
"esbuild-linux-arm64": "0.15.18",
"esbuild-linux-mips64le": "0.15.18",
"esbuild-linux-ppc64le": "0.15.18",
"esbuild-linux-riscv64": "0.15.18",
"esbuild-linux-s390x": "0.15.18",
"esbuild-netbsd-64": "0.15.18",
"esbuild-openbsd-64": "0.15.18",
"esbuild-sunos-64": "0.15.18",
"esbuild-windows-32": "0.15.18",
"esbuild-windows-64": "0.15.18",
"esbuild-windows-arm64": "0.15.18"
}
},
"node_modules/esbuild-android-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz",
"integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-android-arm64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz",
"integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-darwin-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz",
"integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-darwin-arm64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz",
"integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-freebsd-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz",
"integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-freebsd-arm64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz",
"integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-32": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz",
"integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz",
"integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-arm": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz",
"integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-arm64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz",
"integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-mips64le": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz",
"integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-ppc64le": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz",
"integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-riscv64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz",
"integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-s390x": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz",
"integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-netbsd-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz",
"integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-openbsd-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz",
"integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-sunos-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz",
"integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-windows-32": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz",
"integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-windows-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz",
"integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-windows-arm64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz",
"integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/is-core-module": {
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz",
"integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.4"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/nanoid": {
"version": "3.3.20",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
"integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.5.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz",
"integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.20",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/resolve": {
"version": "1.22.13",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.13.tgz",
"integrity": "sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"is-core-module": "^2.17.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/rollup": {
"version": "2.80.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz",
"integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==",
"dev": true,
"license": "MIT",
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=10.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/source-map-js": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
"integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/vite": {
"version": "3.2.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.11.tgz",
"integrity": "sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.15.9",
"postcss": "^8.4.18",
"resolve": "^1.22.1",
"rollup": "^2.79.1"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
},
"peerDependencies": {
"@types/node": ">= 14",
"less": "*",
"sass": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"less": {
"optional": true
},
"sass": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
}
}
}
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"name": "hfb-price-tool-frontend",
"private": true,
"version": "1.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^3.0.7"
}
}
+1
View File
@@ -0,0 +1 @@
93f3bcafbb7b2e5f01e2db048bd59072
+1111
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vite'
// 版本号唯一来源:wails.json 的 info.productVersion。
function readAppVersion() {
try {
const path = fileURLToPath(new URL('../wails.json', import.meta.url))
return JSON.parse(readFileSync(path, 'utf-8')).info?.productVersion || '0.0.0'
} catch {
return '0.0.0'
}
}
export default defineConfig({
define: {
__APP_VERSION__: JSON.stringify(readAppVersion()),
},
server: {
host: 'localhost',
port: 5199,
strictPort: true,
},
})
+20
View File
@@ -0,0 +1,20 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import {main} from '../models';
import {pricing} from '../models';
export function Bootstrap():Promise<main.Bootstrap>;
export function Calculate(arg1:pricing.Input):Promise<pricing.Result>;
export function ExportConfig():Promise<main.ExportResult>;
export function ImportConfig():Promise<main.ImportResult>;
export function OpenConfigFolder():Promise<void>;
export function ResetConfig():Promise<pricing.Config>;
export function SaveConfig(arg1:pricing.Config):Promise<pricing.Config>;
export function SyncFromServer(arg1:string):Promise<main.SyncResult>;
+35
View File
@@ -0,0 +1,35 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function Bootstrap() {
return window['go']['main']['App']['Bootstrap']();
}
export function Calculate(arg1) {
return window['go']['main']['App']['Calculate'](arg1);
}
export function ExportConfig() {
return window['go']['main']['App']['ExportConfig']();
}
export function ImportConfig() {
return window['go']['main']['App']['ImportConfig']();
}
export function OpenConfigFolder() {
return window['go']['main']['App']['OpenConfigFolder']();
}
export function ResetConfig() {
return window['go']['main']['App']['ResetConfig']();
}
export function SaveConfig(arg1) {
return window['go']['main']['App']['SaveConfig'](arg1);
}
export function SyncFromServer(arg1) {
return window['go']['main']['App']['SyncFromServer'](arg1);
}
+357
View File
@@ -0,0 +1,357 @@
export namespace main {
export class Bootstrap {
config: pricing.Config;
config_path: string;
server_url: string;
load_error?: string;
static createFrom(source: any = {}) {
return new Bootstrap(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.config = this.convertValues(source["config"], pricing.Config);
this.config_path = source["config_path"];
this.server_url = source["server_url"];
this.load_error = source["load_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 ExportResult {
path: string;
cancelled: boolean;
static createFrom(source: any = {}) {
return new ExportResult(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.path = source["path"];
this.cancelled = source["cancelled"];
}
}
export class ImportResult {
config: pricing.Config;
path: string;
cancelled: boolean;
static createFrom(source: any = {}) {
return new ImportResult(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.config = this.convertValues(source["config"], pricing.Config);
this.path = source["path"];
this.cancelled = source["cancelled"];
}
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 SyncResult {
config: pricing.Config;
server_url: string;
synced_at: string;
insurance_count: number;
ratio_item_count: number;
coin_correction_count: number;
quantity_item_count: number;
static createFrom(source: any = {}) {
return new SyncResult(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.config = this.convertValues(source["config"], pricing.Config);
this.server_url = source["server_url"];
this.synced_at = source["synced_at"];
this.insurance_count = source["insurance_count"];
this.ratio_item_count = source["ratio_item_count"];
this.coin_correction_count = source["coin_correction_count"];
this.quantity_item_count = source["quantity_item_count"];
}
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 namespace pricing {
export class CoinCorrection {
threshold_m: number;
correction: number;
static createFrom(source: any = {}) {
return new CoinCorrection(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.threshold_m = source["threshold_m"];
this.correction = source["correction"];
}
}
export class QuantityItem {
key: string;
label: string;
price: string;
unit: string;
placeholder?: string;
disabled: boolean;
static createFrom(source: any = {}) {
return new QuantityItem(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.key = source["key"];
this.label = source["label"];
this.price = source["price"];
this.unit = source["unit"];
this.placeholder = source["placeholder"];
this.disabled = source["disabled"];
}
}
export class RatioConfigItem {
key: string;
label: string;
kind: string;
group_key?: string;
missing_penalty: number;
static createFrom(source: any = {}) {
return new RatioConfigItem(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.key = source["key"];
this.label = source["label"];
this.kind = source["kind"];
this.group_key = source["group_key"];
this.missing_penalty = source["missing_penalty"];
}
}
export class InsuranceBaseRatio {
insurance: string;
ratio: number;
static createFrom(source: any = {}) {
return new InsuranceBaseRatio(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.insurance = source["insurance"];
this.ratio = source["ratio"];
}
}
export class Config {
schema_version: number;
insurance_base_ratios: InsuranceBaseRatio[];
ratio_config_items: RatioConfigItem[];
coin_corrections: CoinCorrection[];
quantity_items: QuantityItem[];
daily_loss_options: number[];
accelerated_max_ratio: number;
sync_url: string;
static createFrom(source: any = {}) {
return new Config(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.schema_version = source["schema_version"];
this.insurance_base_ratios = this.convertValues(source["insurance_base_ratios"], InsuranceBaseRatio);
this.ratio_config_items = this.convertValues(source["ratio_config_items"], RatioConfigItem);
this.coin_corrections = this.convertValues(source["coin_corrections"], CoinCorrection);
this.quantity_items = this.convertValues(source["quantity_items"], QuantityItem);
this.daily_loss_options = source["daily_loss_options"];
this.accelerated_max_ratio = source["accelerated_max_ratio"];
this.sync_url = source["sync_url"];
}
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 Input {
coin_m: number;
insurance: string;
states: Record<string, boolean>;
daily_loss_m: number;
sale_ratio: number;
quantities: Record<string, number>;
free_items: Record<string, boolean>;
static createFrom(source: any = {}) {
return new Input(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.coin_m = source["coin_m"];
this.insurance = source["insurance"];
this.states = source["states"];
this.daily_loss_m = source["daily_loss_m"];
this.sale_ratio = source["sale_ratio"];
this.quantities = source["quantities"];
this.free_items = source["free_items"];
}
}
export class ItemLine {
key: string;
label: string;
quantity: number;
unit: string;
unit_price: number;
amount: number;
free: boolean;
disabled: boolean;
static createFrom(source: any = {}) {
return new ItemLine(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.key = source["key"];
this.label = source["label"];
this.quantity = source["quantity"];
this.unit = source["unit"];
this.unit_price = source["unit_price"];
this.amount = source["amount"];
this.free = source["free"];
this.disabled = source["disabled"];
}
}
export class Result {
reference_ratio: number;
final_ratio: number;
max_ratio: number;
insurance_base_ratio: number;
config_penalty: number;
coin_correction: number;
daily_loss_adjustment: number;
coin_base_price: number;
item_price: number;
total_price: number;
items: ItemLine[];
valid: boolean;
message?: string;
static createFrom(source: any = {}) {
return new Result(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.reference_ratio = source["reference_ratio"];
this.final_ratio = source["final_ratio"];
this.max_ratio = source["max_ratio"];
this.insurance_base_ratio = source["insurance_base_ratio"];
this.config_penalty = source["config_penalty"];
this.coin_correction = source["coin_correction"];
this.daily_loss_adjustment = source["daily_loss_adjustment"];
this.coin_base_price = source["coin_base_price"];
this.item_price = source["item_price"];
this.total_price = source["total_price"];
this.items = this.convertValues(source["items"], ItemLine);
this.valid = source["valid"];
this.message = source["message"];
}
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;
}
}
}
+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);
}
+36
View File
@@ -0,0 +1,36 @@
module hfb_price_tool
go 1.25.0
require github.com/wailsapp/wails/v2 v2.15.0
require (
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // 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.53.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.39.0 // indirect
)
+83
View File
@@ -0,0 +1,83 @@
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/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
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/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
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.15.0 h1:u7cHK+UesZOlYxyJxfYLteaCPhws6UsZoDdqUejuX6Q=
github.com/wailsapp/wails/v2 v2.15.0/go.mod h1:scxrgwfsv6yR6fE6cCF+Flfl+JeU+SR87T9x4kILJ6M=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
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-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.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
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.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+140
View File
@@ -0,0 +1,140 @@
package pricing
import (
"math"
"strings"
)
// Calculate 只计算号主到手价:参考比例、加速比例、纯币价、物品价和合计。
func (config Config) Calculate(input Input) Result {
cfg := Normalize(config)
coinM := math.Max(input.CoinM, 0)
coinWan := coinM * 100
result := Result{
Items: []ItemLine{},
Valid: false,
}
baseRatio, ok := insuranceBaseRatio(cfg.InsuranceBaseRatios, input.Insurance)
if !ok {
result.Message = "未找到该保险格数的基础比例,请检查规则配置"
result.ItemPrice, result.Items = calculateItemLines(cfg, input)
result.TotalPrice = result.ItemPrice
return result
}
result.InsuranceBaseRatio = baseRatio
result.ConfigPenalty = calculateConfigPenalty(cfg.RatioConfigItems, input.States)
result.CoinCorrection = coinCorrection(cfg.CoinCorrections, coinM)
result.DailyLossAdjustment = dailyLossRatioAdjustment(input.DailyLossM)
referenceRatio := RoundRatio(
baseRatio +
result.ConfigPenalty +
result.CoinCorrection +
result.DailyLossAdjustment,
)
result.ReferenceRatio = referenceRatio
result.MaxRatio = RoundRatio(referenceRatio + cfg.AcceleratedMaxRatio)
result.FinalRatio = referenceRatio
if input.SaleRatio > 0 {
result.FinalRatio = RoundRatio(clampSaleRatio(input.SaleRatio, referenceRatio, result.MaxRatio))
}
if coinWan > 0 && result.FinalRatio > 0 {
result.CoinBasePrice = RoundMoney(coinWan / result.FinalRatio)
}
result.ItemPrice, result.Items = calculateItemLines(cfg, input)
result.TotalPrice = RoundMoney(result.CoinBasePrice + result.ItemPrice)
result.Valid = referenceRatio > 0
if coinM <= 0 {
result.Message = "请输入哈弗币数量"
}
return result
}
func insuranceBaseRatio(items []InsuranceBaseRatio, insurance string) (float64, bool) {
insurance = strings.TrimSpace(insurance)
for _, item := range items {
if item.Insurance == insurance {
return item.Ratio, true
}
}
return 0, false
}
func calculateConfigPenalty(items []RatioConfigItem, states map[string]bool) float64 {
penalty := 0.0
for _, item := range items {
if states[item.Key] {
continue
}
penalty += item.MissingPenalty
}
return RoundRatio(penalty)
}
// coinCorrection 命中最高阈值档,且使用严格大于,与主站发布逻辑保持一致。
func coinCorrection(items []CoinCorrection, coinM float64) float64 {
bestThreshold := -1.0
correction := 0.0
for _, item := range items {
if coinM > item.ThresholdM && item.ThresholdM >= bestThreshold {
bestThreshold = item.ThresholdM
correction = item.Correction
}
}
return correction
}
func dailyLossRatioAdjustment(dailyLossM float64) float64 {
if dailyLossM <= 0 {
return 0
}
adjustment := (dailyLossM - 10) / 10
if adjustment < 0 {
return 0
}
floored := math.Floor(adjustment)
if floored > 4 {
return 4
}
return floored
}
func clampSaleRatio(requested, reference, maxRatio float64) float64 {
if requested < reference {
return reference
}
if requested > maxRatio {
return maxRatio
}
return requested
}
func calculateItemLines(config Config, input Input) (float64, []ItemLine) {
lines := make([]ItemLine, 0, len(config.QuantityItems))
rawTotal := 0.0
for _, item := range config.QuantityItems {
quantity := math.Max(input.Quantities[item.Key], 0)
unitPrice := ParseUnitPrice(item.Price)
free := input.FreeItems[item.Key]
disabled := isItemDisabledForInsurance(item, input.Insurance)
amount := 0.0
if !disabled && !free && quantity > 0 && unitPrice > 0 {
rawTotal += quantity * unitPrice
amount = RoundMoney(quantity * unitPrice)
}
lines = append(lines, ItemLine{
Key: item.Key,
Label: item.Label,
Quantity: quantity,
Unit: item.Unit,
UnitPrice: unitPrice,
Amount: amount,
Free: free,
Disabled: disabled,
})
}
return RoundMoney(rawTotal), lines
}
+240
View File
@@ -0,0 +1,240 @@
package pricing
import (
"math"
"testing"
)
func TestScreenshotExample(t *testing.T) {
config := DefaultConfig()
result := config.Calculate(Input{
CoinM: 226,
Insurance: "3*3",
States: map[string]bool{},
DailyLossM: 10,
})
if result.ReferenceRatio != 46 {
t.Fatalf("reference ratio = %v, want 46", result.ReferenceRatio)
}
if result.FinalRatio != 46 {
t.Fatalf("final ratio = %v, want 46", result.FinalRatio)
}
if result.CoinBasePrice != 491.3 {
t.Fatalf("coin base price = %v, want 491.3", result.CoinBasePrice)
}
if result.ItemPrice != 0 {
t.Fatalf("item price = %v, want 0", result.ItemPrice)
}
if result.TotalPrice != 491.3 {
t.Fatalf("total price = %v, want 491.3", result.TotalPrice)
}
}
func TestAllStatesMatched(t *testing.T) {
config := DefaultConfig()
result := config.Calculate(Input{
CoinM: 100,
Insurance: "3*3",
States: map[string]bool{
"melee": true,
"operatorRed": true,
"staminaMax": true,
"loadMax": true,
"weapon": true,
},
DailyLossM: 10,
})
if result.ReferenceRatio != 40 {
t.Fatalf("reference ratio = %v, want 40", result.ReferenceRatio)
}
if result.CoinBasePrice != 250 {
t.Fatalf("coin base price = %v, want 250", result.CoinBasePrice)
}
}
func TestMissingPenalty(t *testing.T) {
config := DefaultConfig()
result := config.Calculate(Input{
CoinM: 100,
Insurance: "3*3",
States: map[string]bool{
"melee": true,
"operatorRed": true,
"staminaMax": true,
"loadMax": true,
},
DailyLossM: 10,
})
if result.ConfigPenalty != 1 {
t.Fatalf("config penalty = %v, want 1", result.ConfigPenalty)
}
if result.ReferenceRatio != 41 {
t.Fatalf("reference ratio = %v, want 41", result.ReferenceRatio)
}
if result.CoinBasePrice != 243.9 {
t.Fatalf("coin base price = %v, want 243.9", result.CoinBasePrice)
}
}
func TestCoinCorrectionUsesHighestCrossedThreshold(t *testing.T) {
config := DefaultConfig()
cases := []struct {
coinM float64
want float64
}{
{130, 0},
{130.1, 1},
{230, 1},
{230.1, 2.5},
{380.1, 4},
{600, 5},
}
for _, item := range cases {
result := config.Calculate(Input{CoinM: item.coinM, Insurance: "3*3", DailyLossM: 10})
if result.CoinCorrection != item.want {
t.Fatalf("coinM=%v correction=%v, want %v", item.coinM, result.CoinCorrection, item.want)
}
}
}
func TestDailyLossAdjustment(t *testing.T) {
config := DefaultConfig()
cases := []struct {
loss float64
want float64
}{
{0, 0},
{10, 0},
{20, 1},
{30, 2},
{40, 3},
{50, 4},
{90, 4},
}
for _, item := range cases {
result := config.Calculate(Input{CoinM: 100, Insurance: "3*3", DailyLossM: item.loss})
if result.DailyLossAdjustment != item.want {
t.Fatalf("dailyLoss=%v adjustment=%v, want %v", item.loss, result.DailyLossAdjustment, item.want)
}
}
}
func TestAcceleratedRatioClamp(t *testing.T) {
config := DefaultConfig()
base := Input{CoinM: 226, Insurance: "3*3", DailyLossM: 10}
result := config.Calculate(withSaleRatio(base, 50))
if result.FinalRatio != 50 {
t.Fatalf("final ratio = %v, want 50", result.FinalRatio)
}
if result.MaxRatio != 56 {
t.Fatalf("max ratio = %v, want 56", result.MaxRatio)
}
result = config.Calculate(withSaleRatio(base, 100))
if result.FinalRatio != 56 {
t.Fatalf("clamped ratio = %v, want 56", result.FinalRatio)
}
result = config.Calculate(withSaleRatio(base, 30))
if result.FinalRatio != 46 {
t.Fatalf("ratio below reference = %v, want 46", result.FinalRatio)
}
}
func TestConsumablePriceParsing(t *testing.T) {
cases := []struct {
price string
want float64
}{
{"0.6元/发", 0.6},
{"6元/60发", 0.1},
{"2.5元/个", 2.5},
{"3元/张", 3},
{"2元", 2},
{"", 0},
}
for _, item := range cases {
if got := ParseUnitPrice(item.price); math.Abs(got-item.want) > 1e-9 {
t.Fatalf("ParseUnitPrice(%q)=%v, want %v", item.price, got, item.want)
}
}
}
func TestConsumablesAndFreeItems(t *testing.T) {
config := DefaultConfig()
result := config.Calculate(Input{
CoinM: 100,
Insurance: "3*3",
DailyLossM: 10,
States: map[string]bool{
"melee": true,
"operatorRed": true,
"staminaMax": true,
"loadMax": true,
"weapon": true,
},
Quantities: map[string]float64{
"awmAmmo": 10,
"helmet6": 2,
},
FreeItems: map[string]bool{
"helmet6": true,
},
})
if result.ItemPrice != 6 {
t.Fatalf("item price = %v, want 6", result.ItemPrice)
}
if result.TotalPrice != 256 {
t.Fatalf("total price = %v, want 256", result.TotalPrice)
}
}
func TestGridCardDisabledAtThreeByThree(t *testing.T) {
config := DefaultConfig()
result := config.Calculate(Input{
CoinM: 100,
Insurance: "3*3",
DailyLossM: 10,
Quantities: map[string]float64{"gridCard9": 3},
})
if result.ItemPrice != 0 {
t.Fatalf("item price = %v, want 0", result.ItemPrice)
}
for _, item := range result.Items {
if item.Key == "gridCard9" && !item.Disabled {
t.Fatal("gridCard9 should be disabled at 3*3")
}
}
result = config.Calculate(Input{
CoinM: 100,
Insurance: "2*3",
DailyLossM: 10,
Quantities: map[string]float64{"gridCard9": 3},
})
if result.ItemPrice != 9 {
t.Fatalf("item price at 2*3 = %v, want 9", result.ItemPrice)
}
}
func TestNormalizeRestoresEmptySections(t *testing.T) {
normalized := Normalize(Config{})
if len(normalized.InsuranceBaseRatios) == 0 {
t.Fatal("insurance ratios should fall back to defaults")
}
if len(normalized.RatioConfigItems) == 0 {
t.Fatal("ratio config items should fall back to defaults")
}
if len(normalized.QuantityItems) == 0 {
t.Fatal("quantity items should fall back to defaults")
}
if normalized.AcceleratedMaxRatio != 10 {
t.Fatalf("accelerated max = %v, want 10", normalized.AcceleratedMaxRatio)
}
}
func withSaleRatio(input Input, ratio float64) Input {
input.SaleRatio = ratio
return input
}
+206
View File
@@ -0,0 +1,206 @@
package pricing
import (
"math"
"regexp"
"sort"
"strconv"
"strings"
)
var (
fractionPricePattern = regexp.MustCompile(`(\d+(?:\.\d+)?)\s*元\s*/\s*(\d+(?:\.\d+)?)`)
singlePricePattern = regexp.MustCompile(`(\d+(?:\.\d+)?)\s*元`)
)
// Normalize 保证配置始终可计算,并修补线上同步/手工编辑产生的空字段。
func Normalize(config Config) Config {
defaults := DefaultConfig()
config.SchemaVersion = 1
insurances := make([]InsuranceBaseRatio, 0, len(config.InsuranceBaseRatios))
seenInsurance := make(map[string]struct{}, len(config.InsuranceBaseRatios))
for _, item := range config.InsuranceBaseRatios {
item.Insurance = strings.TrimSpace(item.Insurance)
if item.Insurance == "" || !isFinitePositive(item.Ratio) {
continue
}
if _, exists := seenInsurance[item.Insurance]; exists {
continue
}
seenInsurance[item.Insurance] = struct{}{}
item.Ratio = RoundRatio(item.Ratio)
insurances = append(insurances, item)
}
if len(insurances) == 0 {
insurances = defaults.InsuranceBaseRatios
}
config.InsuranceBaseRatios = insurances
ratioItems := make([]RatioConfigItem, 0, len(config.RatioConfigItems))
seenRatioKey := make(map[string]struct{}, len(config.RatioConfigItems))
for _, item := range config.RatioConfigItems {
item.Key = strings.TrimSpace(item.Key)
item.Label = strings.TrimSpace(item.Label)
item.Kind = strings.TrimSpace(item.Kind)
item.GroupKey = strings.TrimSpace(item.GroupKey)
if item.Key == "" || item.Label == "" || !isFiniteNonNegative(item.MissingPenalty) {
continue
}
if _, exists := seenRatioKey[item.Key]; exists {
continue
}
seenRatioKey[item.Key] = struct{}{}
configKind := item.Kind
if configKind != "skin_group" && configKind != "max_stamina" && configKind != "max_load" {
configKind = "skin_group"
}
item.Kind = configKind
item.MissingPenalty = RoundRatio(item.MissingPenalty)
ratioItems = append(ratioItems, item)
}
if len(ratioItems) == 0 {
ratioItems = defaults.RatioConfigItems
}
config.RatioConfigItems = ratioItems
corrections := make([]CoinCorrection, 0, len(config.CoinCorrections))
for _, item := range config.CoinCorrections {
if !isFiniteNonNegative(item.ThresholdM) || !isFiniteNonNegative(item.Correction) {
continue
}
item.ThresholdM = RoundRatio(item.ThresholdM)
item.Correction = RoundRatio(item.Correction)
corrections = append(corrections, item)
}
if len(corrections) == 0 {
corrections = defaults.CoinCorrections
}
sort.SliceStable(corrections, func(i, j int) bool {
return corrections[i].ThresholdM < corrections[j].ThresholdM
})
config.CoinCorrections = corrections
quantityItems := make([]QuantityItem, 0, len(config.QuantityItems))
seenQuantityKey := make(map[string]struct{}, len(config.QuantityItems))
for _, item := range config.QuantityItems {
item.Key = strings.TrimSpace(item.Key)
item.Label = strings.TrimSpace(item.Label)
item.Price = strings.TrimSpace(item.Price)
item.Unit = strings.TrimSpace(item.Unit)
if item.Key == "" || item.Label == "" || item.Price == "" {
continue
}
if _, exists := seenQuantityKey[item.Key]; exists {
continue
}
seenQuantityKey[item.Key] = struct{}{}
if item.Unit == "" {
item.Unit = defaultUnit(item.Key, item.Label)
}
quantityItems = append(quantityItems, item)
}
if len(quantityItems) == 0 {
quantityItems = defaults.QuantityItems
}
config.QuantityItems = quantityItems
lossOptions := make([]float64, 0, len(config.DailyLossOptions))
seenLoss := make(map[float64]struct{}, len(config.DailyLossOptions))
for _, value := range config.DailyLossOptions {
if !isFinitePositive(value) {
continue
}
value = RoundRatio(value)
if _, exists := seenLoss[value]; exists {
continue
}
seenLoss[value] = struct{}{}
lossOptions = append(lossOptions, value)
}
if len(lossOptions) == 0 {
lossOptions = defaults.DailyLossOptions
}
sort.Float64s(lossOptions)
config.DailyLossOptions = lossOptions
if !isFinitePositive(config.AcceleratedMaxRatio) {
config.AcceleratedMaxRatio = defaults.AcceleratedMaxRatio
}
config.AcceleratedMaxRatio = math.Min(RoundRatio(config.AcceleratedMaxRatio), 50)
config.SyncURL = strings.TrimRight(strings.TrimSpace(config.SyncURL), "/")
if config.SyncURL == "" {
config.SyncURL = defaults.SyncURL
}
return config
}
// ParseUnitPrice 解析「0.6元/发」「6元/60发」这类单价文本,返回单个物品的价格。
func ParseUnitPrice(price string) float64 {
normalized := strings.ReplaceAll(strings.TrimSpace(price), "/", "/")
normalized = strings.ReplaceAll(normalized, ",", ",")
if match := fractionPricePattern.FindStringSubmatch(normalized); len(match) == 3 {
amount := parseFloat(match[1])
count := parseFloat(match[2])
if amount > 0 && count > 0 {
return amount / count
}
}
if match := singlePricePattern.FindStringSubmatch(normalized); len(match) == 2 {
return math.Max(parseFloat(match[1]), 0)
}
return 0
}
func RoundMoney(value float64) float64 {
return math.Round(value*10) / 10
}
func RoundRatio(value float64) float64 {
return math.Round(value*10) / 10
}
func isItemDisabledForInsurance(item QuantityItem, insurance string) bool {
if item.Disabled {
return true
}
if insurance != "3*3" {
return false
}
return item.Key == "gridCard9" ||
strings.Contains(item.Label, "9格") ||
strings.Contains(item.Label, "顶级保险卡")
}
func defaultUnit(key, label string) string {
switch key {
case "awmAmmo", "level6Ammo", "barrettAmmo":
return "发"
case "gridCard9":
return "张"
}
if strings.Contains(label, "子弹") {
return "发"
}
if strings.Contains(label, "卡") {
return "张"
}
return "个"
}
func parseFloat(value string) float64 {
result, err := strconv.ParseFloat(strings.TrimSpace(value), 64)
if err != nil || math.IsNaN(result) || math.IsInf(result, 0) {
return 0
}
return result
}
func isFinitePositive(value float64) bool {
return value > 0 && !math.IsNaN(value) && !math.IsInf(value, 0)
}
func isFiniteNonNegative(value float64) bool {
return value >= 0 && !math.IsNaN(value) && !math.IsInf(value, 0)
}
+42
View File
@@ -0,0 +1,42 @@
package pricing
const DefaultSyncURL = "https://hao858.com"
func DefaultConfig() Config {
return Config{
SchemaVersion: 1,
InsuranceBaseRatios: []InsuranceBaseRatio{
{Insurance: "3*3", Ratio: 40},
{Insurance: "2*3", Ratio: 42},
{Insurance: "2*2", Ratio: 47},
{Insurance: "2*1", Ratio: 48},
},
RatioConfigItems: []RatioConfigItem{
{Key: "melee", Label: "刀皮", Kind: "skin_group", GroupKey: "melee", MissingPenalty: 1},
{Key: "operatorRed", Label: "角色红皮", Kind: "skin_group", GroupKey: "operatorRed", MissingPenalty: 1},
{Key: "staminaMax", Label: "满体力", Kind: "max_stamina", MissingPenalty: 1},
{Key: "loadMax", Label: "满负重", Kind: "max_load", MissingPenalty: 1},
{Key: "weapon", Label: "武器皮肤", Kind: "skin_group", GroupKey: "weapon", MissingPenalty: 1},
},
CoinCorrections: []CoinCorrection{
{ThresholdM: 130, Correction: 1},
{ThresholdM: 230, Correction: 2.5},
{ThresholdM: 380, Correction: 4},
{ThresholdM: 530, Correction: 5},
},
QuantityItems: []QuantityItem{
{Key: "awmAmmo", Label: "AWM子弹", Price: "0.6元/发", Unit: "发"},
{Key: "level6Ammo", Label: "6级子弹", Price: "0.1元/发", Unit: "发", Placeholder: "按单发数量填写,60发=6元"},
{Key: "helmet6", Label: "6级头盔", Price: "1.5元/个", Unit: "个"},
{Key: "armor6", Label: "6级护甲", Price: "2.5元/个", Unit: "个"},
{Key: "barrettAmmo", Label: "巴雷特子弹", Price: "0.3元/发", Unit: "发"},
{Key: "gridCard9", Label: "顶级保险卡", Price: "3元/张", Unit: "张", Placeholder: "3*3 保险下不可用"},
{Key: "coffeeBean", Label: "高级咖啡豆", Price: "2.5元/个", Unit: "个"},
{Key: "bulletPart", Label: "高级子弹零件", Price: "3元/个", Unit: "个"},
{Key: "redPacket45", Label: "45格大红包", Price: "1.5元/个", Unit: "个"},
},
DailyLossOptions: []float64{10, 20, 30, 40, 50},
AcceleratedMaxRatio: 10,
SyncURL: DefaultSyncURL,
}
}
+76
View File
@@ -0,0 +1,76 @@
package pricing
type InsuranceBaseRatio struct {
Insurance string `json:"insurance"`
Ratio float64 `json:"ratio"`
}
type RatioConfigItem struct {
Key string `json:"key"`
Label string `json:"label"`
Kind string `json:"kind"`
GroupKey string `json:"group_key,omitempty"`
MissingPenalty float64 `json:"missing_penalty"`
}
type CoinCorrection struct {
ThresholdM float64 `json:"threshold_m"`
Correction float64 `json:"correction"`
}
type QuantityItem struct {
Key string `json:"key"`
Label string `json:"label"`
Price string `json:"price"`
Unit string `json:"unit"`
Placeholder string `json:"placeholder,omitempty"`
Disabled bool `json:"disabled"`
}
type Config struct {
SchemaVersion int `json:"schema_version"`
InsuranceBaseRatios []InsuranceBaseRatio `json:"insurance_base_ratios"`
RatioConfigItems []RatioConfigItem `json:"ratio_config_items"`
CoinCorrections []CoinCorrection `json:"coin_corrections"`
QuantityItems []QuantityItem `json:"quantity_items"`
DailyLossOptions []float64 `json:"daily_loss_options"`
AcceleratedMaxRatio float64 `json:"accelerated_max_ratio"`
SyncURL string `json:"sync_url"`
}
type Input struct {
CoinM float64 `json:"coin_m"`
Insurance string `json:"insurance"`
States map[string]bool `json:"states"`
DailyLossM float64 `json:"daily_loss_m"`
SaleRatio float64 `json:"sale_ratio"`
Quantities map[string]float64 `json:"quantities"`
FreeItems map[string]bool `json:"free_items"`
}
type ItemLine struct {
Key string `json:"key"`
Label string `json:"label"`
Quantity float64 `json:"quantity"`
Unit string `json:"unit"`
UnitPrice float64 `json:"unit_price"`
Amount float64 `json:"amount"`
Free bool `json:"free"`
Disabled bool `json:"disabled"`
}
type Result struct {
ReferenceRatio float64 `json:"reference_ratio"`
FinalRatio float64 `json:"final_ratio"`
MaxRatio float64 `json:"max_ratio"`
InsuranceBaseRatio float64 `json:"insurance_base_ratio"`
ConfigPenalty float64 `json:"config_penalty"`
CoinCorrection float64 `json:"coin_correction"`
DailyLossAdjustment float64 `json:"daily_loss_adjustment"`
CoinBasePrice float64 `json:"coin_base_price"`
ItemPrice float64 `json:"item_price"`
TotalPrice float64 `json:"total_price"`
Items []ItemLine `json:"items"`
Valid bool `json:"valid"`
Message string `json:"message,omitempty"`
}
+35
View File
@@ -0,0 +1,35 @@
package main
import (
"embed"
"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
func main() {
app := NewApp()
err := wails.Run(&options.App{
Title: "哈夫币发布计算器",
Width: 1120,
Height: 760,
MinWidth: 980,
MinHeight: 640,
AssetServer: &assetserver.Options{
Assets: assets,
},
BackgroundColour: &options.RGBA{R: 243, G: 246, B: 250, A: 1},
OnStartup: app.startup,
Bind: []interface{}{
app,
},
})
if err != nil {
println("Error:", err.Error())
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"$schema": "https://wails.io/schemas/config.v2.json",
"name": "hfb_price_tool",
"outputfilename": "hfb_price_tool",
"info": {
"companyName": "yml",
"productName": "哈夫币发布计算器",
"productVersion": "1.0.1",
"copyright": "Copyright © 2026 yml",
"comments": "按账号配置计算号主到手价(界面与 exe 版本号均取自 info.productVersion)"
},
"frontend:install": "npm install",
"frontend:build": "npm run build",
"frontend:dev:watcher": "npm run dev",
"frontend:dev:serverUrl": "http://localhost:5199",
"devServer": "localhost:34199",
"author": {
"name": "yml",
"email": ""
}
}