1.0.1
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
frontend/node_modules/
|
||||
frontend/dist/
|
||||
build/bin/
|
||||
.DS_Store
|
||||
*.log
|
||||
@@ -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
|
||||
@@ -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 样式
|
||||
```
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
@@ -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 |
@@ -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}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Generated
+664
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
93f3bcafbb7b2e5f01e2db048bd59072
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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>;
|
||||
Executable
+35
@@ -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);
|
||||
}
|
||||
Executable
+357
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@wailsapp/runtime",
|
||||
"version": "2.0.0",
|
||||
"description": "Wails Javascript runtime library",
|
||||
"main": "runtime.js",
|
||||
"types": "runtime.d.ts",
|
||||
"scripts": {
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wailsapp/wails.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Wails",
|
||||
"Javascript",
|
||||
"Go"
|
||||
],
|
||||
"author": "Lea Anthony <lea.anthony@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wailsapp/wails/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||
}
|
||||
+330
@@ -0,0 +1,330 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export interface Screen {
|
||||
isCurrent: boolean;
|
||||
isPrimary: boolean;
|
||||
width : number
|
||||
height : number
|
||||
}
|
||||
|
||||
// Environment information such as platform, buildtype, ...
|
||||
export interface EnvironmentInfo {
|
||||
buildType: string;
|
||||
platform: string;
|
||||
arch: string;
|
||||
}
|
||||
|
||||
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
|
||||
// emits the given event. Optional data may be passed with the event.
|
||||
// This will trigger any event listeners.
|
||||
export function EventsEmit(eventName: string, ...data: any): void;
|
||||
|
||||
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
|
||||
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
|
||||
// sets up a listener for the given event name, but will only trigger a given number times.
|
||||
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
|
||||
|
||||
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
|
||||
// sets up a listener for the given event name, but will only trigger once.
|
||||
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
|
||||
// unregisters the listener for the given event name.
|
||||
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
|
||||
|
||||
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
|
||||
// unregisters all listeners.
|
||||
export function EventsOffAll(): void;
|
||||
|
||||
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
|
||||
// logs the given message as a raw message
|
||||
export function LogPrint(message: string): void;
|
||||
|
||||
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
|
||||
// logs the given message at the `trace` log level.
|
||||
export function LogTrace(message: string): void;
|
||||
|
||||
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
|
||||
// logs the given message at the `debug` log level.
|
||||
export function LogDebug(message: string): void;
|
||||
|
||||
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
|
||||
// logs the given message at the `error` log level.
|
||||
export function LogError(message: string): void;
|
||||
|
||||
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
|
||||
// logs the given message at the `fatal` log level.
|
||||
// The application will quit after calling this method.
|
||||
export function LogFatal(message: string): void;
|
||||
|
||||
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
|
||||
// logs the given message at the `info` log level.
|
||||
export function LogInfo(message: string): void;
|
||||
|
||||
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
|
||||
// logs the given message at the `warning` log level.
|
||||
export function LogWarning(message: string): void;
|
||||
|
||||
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
|
||||
// Forces a reload by the main application as well as connected browsers.
|
||||
export function WindowReload(): void;
|
||||
|
||||
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
|
||||
// Reloads the application frontend.
|
||||
export function WindowReloadApp(): void;
|
||||
|
||||
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
|
||||
// Sets the window AlwaysOnTop or not on top.
|
||||
export function WindowSetAlwaysOnTop(b: boolean): void;
|
||||
|
||||
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
|
||||
// *Windows only*
|
||||
// Sets window theme to system default (dark/light).
|
||||
export function WindowSetSystemDefaultTheme(): void;
|
||||
|
||||
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
|
||||
// *Windows only*
|
||||
// Sets window to light theme.
|
||||
export function WindowSetLightTheme(): void;
|
||||
|
||||
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
|
||||
// *Windows only*
|
||||
// Sets window to dark theme.
|
||||
export function WindowSetDarkTheme(): void;
|
||||
|
||||
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
|
||||
// Centers the window on the monitor the window is currently on.
|
||||
export function WindowCenter(): void;
|
||||
|
||||
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
|
||||
// Sets the text in the window title bar.
|
||||
export function WindowSetTitle(title: string): void;
|
||||
|
||||
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
|
||||
// Makes the window full screen.
|
||||
export function WindowFullscreen(): void;
|
||||
|
||||
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
|
||||
// Restores the previous window dimensions and position prior to full screen.
|
||||
export function WindowUnfullscreen(): void;
|
||||
|
||||
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
|
||||
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
|
||||
export function WindowIsFullscreen(): Promise<boolean>;
|
||||
|
||||
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||
// Sets the width and height of the window.
|
||||
export function WindowSetSize(width: number, height: number): void;
|
||||
|
||||
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||
// Gets the width and height of the window.
|
||||
export function WindowGetSize(): Promise<Size>;
|
||||
|
||||
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
|
||||
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMaxSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
|
||||
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMinSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
|
||||
// Sets the window position relative to the monitor the window is currently on.
|
||||
export function WindowSetPosition(x: number, y: number): void;
|
||||
|
||||
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
|
||||
// Gets the window position relative to the monitor the window is currently on.
|
||||
export function WindowGetPosition(): Promise<Position>;
|
||||
|
||||
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
|
||||
// Hides the window.
|
||||
export function WindowHide(): void;
|
||||
|
||||
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
|
||||
// Shows the window, if it is currently hidden.
|
||||
export function WindowShow(): void;
|
||||
|
||||
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
|
||||
// Maximises the window to fill the screen.
|
||||
export function WindowMaximise(): void;
|
||||
|
||||
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
|
||||
// Toggles between Maximised and UnMaximised.
|
||||
export function WindowToggleMaximise(): void;
|
||||
|
||||
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
|
||||
// Restores the window to the dimensions and position prior to maximising.
|
||||
export function WindowUnmaximise(): void;
|
||||
|
||||
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
|
||||
// Returns the state of the window, i.e. whether the window is maximised or not.
|
||||
export function WindowIsMaximised(): Promise<boolean>;
|
||||
|
||||
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
|
||||
// Minimises the window.
|
||||
export function WindowMinimise(): void;
|
||||
|
||||
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
|
||||
// Restores the window to the dimensions and position prior to minimising.
|
||||
export function WindowUnminimise(): void;
|
||||
|
||||
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
|
||||
// Returns the state of the window, i.e. whether the window is minimised or not.
|
||||
export function WindowIsMinimised(): Promise<boolean>;
|
||||
|
||||
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
|
||||
// Returns the state of the window, i.e. whether the window is normal or not.
|
||||
export function WindowIsNormal(): Promise<boolean>;
|
||||
|
||||
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
|
||||
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
|
||||
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
|
||||
|
||||
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
|
||||
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
|
||||
export function ScreenGetAll(): Promise<Screen[]>;
|
||||
|
||||
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
|
||||
// Opens the given URL in the system browser.
|
||||
export function BrowserOpenURL(url: string): void;
|
||||
|
||||
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
|
||||
// Returns information about the environment
|
||||
export function Environment(): Promise<EnvironmentInfo>;
|
||||
|
||||
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
|
||||
// Quits the application.
|
||||
export function Quit(): void;
|
||||
|
||||
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
|
||||
// Hides the application.
|
||||
export function Hide(): void;
|
||||
|
||||
// [Show](https://wails.io/docs/reference/runtime/intro#show)
|
||||
// Shows the application.
|
||||
export function Show(): void;
|
||||
|
||||
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
|
||||
// Returns the current text stored on clipboard
|
||||
export function ClipboardGetText(): Promise<string>;
|
||||
|
||||
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||
// Sets a text on the clipboard
|
||||
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||
|
||||
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
|
||||
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
|
||||
|
||||
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
|
||||
// OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
export function OnFileDropOff() :void
|
||||
|
||||
// Check if the file path resolver is available
|
||||
export function CanResolveFilePaths(): boolean;
|
||||
|
||||
// Resolves file paths for an array of files
|
||||
export function ResolveFilePaths(files: File[]): void
|
||||
|
||||
// Notification types
|
||||
export interface NotificationOptions {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string; // macOS and Linux only
|
||||
body?: string;
|
||||
categoryId?: string;
|
||||
data?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface NotificationAction {
|
||||
id?: string;
|
||||
title?: string;
|
||||
destructive?: boolean; // macOS-specific
|
||||
}
|
||||
|
||||
export interface NotificationCategory {
|
||||
id?: string;
|
||||
actions?: NotificationAction[];
|
||||
hasReplyField?: boolean;
|
||||
replyPlaceholder?: string;
|
||||
replyButtonTitle?: string;
|
||||
}
|
||||
|
||||
// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications)
|
||||
// Initializes the notification service for the application.
|
||||
// This must be called before sending any notifications.
|
||||
export function InitializeNotifications(): Promise<void>;
|
||||
|
||||
// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications)
|
||||
// Cleans up notification resources and releases any held connections.
|
||||
export function CleanupNotifications(): Promise<void>;
|
||||
|
||||
// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable)
|
||||
// Checks if notifications are available on the current platform.
|
||||
export function IsNotificationAvailable(): Promise<boolean>;
|
||||
|
||||
// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization)
|
||||
// Requests notification authorization from the user (macOS only).
|
||||
export function RequestNotificationAuthorization(): Promise<boolean>;
|
||||
|
||||
// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization)
|
||||
// Checks the current notification authorization status (macOS only).
|
||||
export function CheckNotificationAuthorization(): Promise<boolean>;
|
||||
|
||||
// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification)
|
||||
// Sends a basic notification with the given options.
|
||||
export function SendNotification(options: NotificationOptions): Promise<void>;
|
||||
|
||||
// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions)
|
||||
// Sends a notification with action buttons. Requires a registered category.
|
||||
export function SendNotificationWithActions(options: NotificationOptions): Promise<void>;
|
||||
|
||||
// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory)
|
||||
// Registers a notification category that can be used with SendNotificationWithActions.
|
||||
export function RegisterNotificationCategory(category: NotificationCategory): Promise<void>;
|
||||
|
||||
// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory)
|
||||
// Removes a previously registered notification category.
|
||||
export function RemoveNotificationCategory(categoryId: string): Promise<void>;
|
||||
|
||||
// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications)
|
||||
// Removes all pending notifications from the notification center.
|
||||
export function RemoveAllPendingNotifications(): Promise<void>;
|
||||
|
||||
// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification)
|
||||
// Removes a specific pending notification by its identifier.
|
||||
export function RemovePendingNotification(identifier: string): Promise<void>;
|
||||
|
||||
// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications)
|
||||
// Removes all delivered notifications from the notification center.
|
||||
export function RemoveAllDeliveredNotifications(): Promise<void>;
|
||||
|
||||
// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification)
|
||||
// Removes a specific delivered notification by its identifier.
|
||||
export function RemoveDeliveredNotification(identifier: string): Promise<void>;
|
||||
|
||||
// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification)
|
||||
// Removes a notification by its identifier (cross-platform convenience function).
|
||||
export function RemoveNotification(identifier: string): Promise<void>;
|
||||
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export function LogPrint(message) {
|
||||
window.runtime.LogPrint(message);
|
||||
}
|
||||
|
||||
export function LogTrace(message) {
|
||||
window.runtime.LogTrace(message);
|
||||
}
|
||||
|
||||
export function LogDebug(message) {
|
||||
window.runtime.LogDebug(message);
|
||||
}
|
||||
|
||||
export function LogInfo(message) {
|
||||
window.runtime.LogInfo(message);
|
||||
}
|
||||
|
||||
export function LogWarning(message) {
|
||||
window.runtime.LogWarning(message);
|
||||
}
|
||||
|
||||
export function LogError(message) {
|
||||
window.runtime.LogError(message);
|
||||
}
|
||||
|
||||
export function LogFatal(message) {
|
||||
window.runtime.LogFatal(message);
|
||||
}
|
||||
|
||||
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
|
||||
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||
}
|
||||
|
||||
export function EventsOn(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, -1);
|
||||
}
|
||||
|
||||
export function EventsOff(eventName, ...additionalEventNames) {
|
||||
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||
}
|
||||
|
||||
export function EventsOffAll() {
|
||||
return window.runtime.EventsOffAll();
|
||||
}
|
||||
|
||||
export function EventsOnce(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, 1);
|
||||
}
|
||||
|
||||
export function EventsEmit(eventName) {
|
||||
let args = [eventName].slice.call(arguments);
|
||||
return window.runtime.EventsEmit.apply(null, args);
|
||||
}
|
||||
|
||||
export function WindowReload() {
|
||||
window.runtime.WindowReload();
|
||||
}
|
||||
|
||||
export function WindowReloadApp() {
|
||||
window.runtime.WindowReloadApp();
|
||||
}
|
||||
|
||||
export function WindowSetAlwaysOnTop(b) {
|
||||
window.runtime.WindowSetAlwaysOnTop(b);
|
||||
}
|
||||
|
||||
export function WindowSetSystemDefaultTheme() {
|
||||
window.runtime.WindowSetSystemDefaultTheme();
|
||||
}
|
||||
|
||||
export function WindowSetLightTheme() {
|
||||
window.runtime.WindowSetLightTheme();
|
||||
}
|
||||
|
||||
export function WindowSetDarkTheme() {
|
||||
window.runtime.WindowSetDarkTheme();
|
||||
}
|
||||
|
||||
export function WindowCenter() {
|
||||
window.runtime.WindowCenter();
|
||||
}
|
||||
|
||||
export function WindowSetTitle(title) {
|
||||
window.runtime.WindowSetTitle(title);
|
||||
}
|
||||
|
||||
export function WindowFullscreen() {
|
||||
window.runtime.WindowFullscreen();
|
||||
}
|
||||
|
||||
export function WindowUnfullscreen() {
|
||||
window.runtime.WindowUnfullscreen();
|
||||
}
|
||||
|
||||
export function WindowIsFullscreen() {
|
||||
return window.runtime.WindowIsFullscreen();
|
||||
}
|
||||
|
||||
export function WindowGetSize() {
|
||||
return window.runtime.WindowGetSize();
|
||||
}
|
||||
|
||||
export function WindowSetSize(width, height) {
|
||||
window.runtime.WindowSetSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMaxSize(width, height) {
|
||||
window.runtime.WindowSetMaxSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMinSize(width, height) {
|
||||
window.runtime.WindowSetMinSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetPosition(x, y) {
|
||||
window.runtime.WindowSetPosition(x, y);
|
||||
}
|
||||
|
||||
export function WindowGetPosition() {
|
||||
return window.runtime.WindowGetPosition();
|
||||
}
|
||||
|
||||
export function WindowHide() {
|
||||
window.runtime.WindowHide();
|
||||
}
|
||||
|
||||
export function WindowShow() {
|
||||
window.runtime.WindowShow();
|
||||
}
|
||||
|
||||
export function WindowMaximise() {
|
||||
window.runtime.WindowMaximise();
|
||||
}
|
||||
|
||||
export function WindowToggleMaximise() {
|
||||
window.runtime.WindowToggleMaximise();
|
||||
}
|
||||
|
||||
export function WindowUnmaximise() {
|
||||
window.runtime.WindowUnmaximise();
|
||||
}
|
||||
|
||||
export function WindowIsMaximised() {
|
||||
return window.runtime.WindowIsMaximised();
|
||||
}
|
||||
|
||||
export function WindowMinimise() {
|
||||
window.runtime.WindowMinimise();
|
||||
}
|
||||
|
||||
export function WindowUnminimise() {
|
||||
window.runtime.WindowUnminimise();
|
||||
}
|
||||
|
||||
export function WindowSetBackgroundColour(R, G, B, A) {
|
||||
window.runtime.WindowSetBackgroundColour(R, G, B, A);
|
||||
}
|
||||
|
||||
export function ScreenGetAll() {
|
||||
return window.runtime.ScreenGetAll();
|
||||
}
|
||||
|
||||
export function WindowIsMinimised() {
|
||||
return window.runtime.WindowIsMinimised();
|
||||
}
|
||||
|
||||
export function WindowIsNormal() {
|
||||
return window.runtime.WindowIsNormal();
|
||||
}
|
||||
|
||||
export function BrowserOpenURL(url) {
|
||||
window.runtime.BrowserOpenURL(url);
|
||||
}
|
||||
|
||||
export function Environment() {
|
||||
return window.runtime.Environment();
|
||||
}
|
||||
|
||||
export function Quit() {
|
||||
window.runtime.Quit();
|
||||
}
|
||||
|
||||
export function Hide() {
|
||||
window.runtime.Hide();
|
||||
}
|
||||
|
||||
export function Show() {
|
||||
window.runtime.Show();
|
||||
}
|
||||
|
||||
export function ClipboardGetText() {
|
||||
return window.runtime.ClipboardGetText();
|
||||
}
|
||||
|
||||
export function ClipboardSetText(text) {
|
||||
return window.runtime.ClipboardSetText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
*
|
||||
* @export
|
||||
* @callback OnFileDropCallback
|
||||
* @param {number} x - x coordinate of the drop
|
||||
* @param {number} y - y coordinate of the drop
|
||||
* @param {string[]} paths - A list of file paths.
|
||||
*/
|
||||
|
||||
/**
|
||||
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
*
|
||||
* @export
|
||||
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
|
||||
*/
|
||||
export function OnFileDrop(callback, useDropTarget) {
|
||||
return window.runtime.OnFileDrop(callback, useDropTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
*/
|
||||
export function OnFileDropOff() {
|
||||
return window.runtime.OnFileDropOff();
|
||||
}
|
||||
|
||||
export function CanResolveFilePaths() {
|
||||
return window.runtime.CanResolveFilePaths();
|
||||
}
|
||||
|
||||
export function ResolveFilePaths(files) {
|
||||
return window.runtime.ResolveFilePaths(files);
|
||||
}
|
||||
|
||||
export function InitializeNotifications() {
|
||||
return window.runtime.InitializeNotifications();
|
||||
}
|
||||
|
||||
export function CleanupNotifications() {
|
||||
return window.runtime.CleanupNotifications();
|
||||
}
|
||||
|
||||
export function IsNotificationAvailable() {
|
||||
return window.runtime.IsNotificationAvailable();
|
||||
}
|
||||
|
||||
export function RequestNotificationAuthorization() {
|
||||
return window.runtime.RequestNotificationAuthorization();
|
||||
}
|
||||
|
||||
export function CheckNotificationAuthorization() {
|
||||
return window.runtime.CheckNotificationAuthorization();
|
||||
}
|
||||
|
||||
export function SendNotification(options) {
|
||||
return window.runtime.SendNotification(options);
|
||||
}
|
||||
|
||||
export function SendNotificationWithActions(options) {
|
||||
return window.runtime.SendNotificationWithActions(options);
|
||||
}
|
||||
|
||||
export function RegisterNotificationCategory(category) {
|
||||
return window.runtime.RegisterNotificationCategory(category);
|
||||
}
|
||||
|
||||
export function RemoveNotificationCategory(categoryId) {
|
||||
return window.runtime.RemoveNotificationCategory(categoryId);
|
||||
}
|
||||
|
||||
export function RemoveAllPendingNotifications() {
|
||||
return window.runtime.RemoveAllPendingNotifications();
|
||||
}
|
||||
|
||||
export function RemovePendingNotification(identifier) {
|
||||
return window.runtime.RemovePendingNotification(identifier);
|
||||
}
|
||||
|
||||
export function RemoveAllDeliveredNotifications() {
|
||||
return window.runtime.RemoveAllDeliveredNotifications();
|
||||
}
|
||||
|
||||
export function RemoveDeliveredNotification(identifier) {
|
||||
return window.runtime.RemoveDeliveredNotification(identifier);
|
||||
}
|
||||
|
||||
export function RemoveNotification(identifier) {
|
||||
return window.runtime.RemoveNotification(identifier);
|
||||
}
|
||||
@@ -0,0 +1,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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
@@ -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": ""
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user