From ccd51b91fb8e8b16e2e2e1138436cb0dbc9c00d3 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 25 Oct 2026 15:02:41 +0800 Subject: [PATCH] 1.0.1 --- .gitignore | 5 + Makefile | 16 + README.md | 155 ++++ app.go | 447 +++++++++ app_test.go | 123 +++ build/appicon.png | Bin 0 -> 82980 bytes build/darwin/Info.plist | 63 ++ build/windows/icon.ico | Bin 0 -> 46072 bytes build/windows/info.json | 16 + build/windows/wails.exe.manifest | 15 + frontend/index.html | 12 + frontend/package-lock.json | 664 ++++++++++++++ frontend/package.json | 14 + frontend/package.json.md5 | 1 + frontend/src/main.js | 1111 +++++++++++++++++++++++ frontend/src/style.css | 1202 +++++++++++++++++++++++++ frontend/vite.config.js | 24 + frontend/wailsjs/go/main/App.d.ts | 20 + frontend/wailsjs/go/main/App.js | 35 + frontend/wailsjs/go/models.ts | 357 ++++++++ frontend/wailsjs/runtime/package.json | 24 + frontend/wailsjs/runtime/runtime.d.ts | 330 +++++++ frontend/wailsjs/runtime/runtime.js | 298 ++++++ go.mod | 36 + go.sum | 83 ++ internal/pricing/calc.go | 140 +++ internal/pricing/calc_test.go | 240 +++++ internal/pricing/config.go | 206 +++++ internal/pricing/defaults.go | 42 + internal/pricing/types.go | 76 ++ main.go | 35 + wails.json | 21 + 32 files changed, 5811 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 app.go create mode 100644 app_test.go create mode 100644 build/appicon.png create mode 100644 build/darwin/Info.plist create mode 100644 build/windows/icon.ico create mode 100644 build/windows/info.json create mode 100644 build/windows/wails.exe.manifest create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100755 frontend/package.json.md5 create mode 100644 frontend/src/main.js create mode 100644 frontend/src/style.css create mode 100644 frontend/vite.config.js create mode 100755 frontend/wailsjs/go/main/App.d.ts create mode 100755 frontend/wailsjs/go/main/App.js create mode 100755 frontend/wailsjs/go/models.ts create mode 100644 frontend/wailsjs/runtime/package.json create mode 100644 frontend/wailsjs/runtime/runtime.d.ts create mode 100644 frontend/wailsjs/runtime/runtime.js create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/pricing/calc.go create mode 100644 internal/pricing/calc_test.go create mode 100644 internal/pricing/config.go create mode 100644 internal/pricing/defaults.go create mode 100644 internal/pricing/types.go create mode 100644 main.go create mode 100644 wails.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..815b9a5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +frontend/node_modules/ +frontend/dist/ +build/bin/ +.DS_Store +*.log diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e2278a0 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..44a337d --- /dev/null +++ b/README.md @@ -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 样式 +``` diff --git a/app.go b/app.go new file mode 100644 index 0000000..da8c6f2 --- /dev/null +++ b/app.go @@ -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] + "..." +} diff --git a/app_test.go b/app_test.go new file mode 100644 index 0000000..e19f140 --- /dev/null +++ b/app_test.go @@ -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") + } +} diff --git a/build/appicon.png b/build/appicon.png new file mode 100644 index 0000000000000000000000000000000000000000..90e62f20e4e15cc4edbeacb513cc8a177c94bebd GIT binary patch literal 82980 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%0AWYW<#wVlh+f{)gr$lbw44x6Mj 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�CYDEYU@^qu3QRRaBjPBupD%<5Ac9-R}^ zq1m^X`(YxQY;G`Hbxfs?U_*^UOmy_Vab!?X=|jR_zxS+pR;~@az|4JcU$xR6#o9q2)~`@;PXih0;3X$90O&?Af?&r$|5!!y1m1Gapypb}1|2Gqs)BjBJHPAq$etB# zbZS(J9c087<1gZiA!sv0DfHt>23~T30x63jo13N3eHKI0A6zQo53`~C7X`x%{!>RdgZs|39dq{U@m*lEI>sbeo(g zNlZgS5}D2!yFb|N80qd8jd}CNPZ#x!<$B|Piu4bc-~435`29o)Iv8Bj_sNx_HP7!_ z7oF8*p4tpBq@sC$g^$7L9&84M_`X25b{o*g<=cxw!qjmsK;`j+5|nw*2eEbGkV&O zN_6h^rBv2gW8e?JThFXJ56AY6)}4Y|@Gm6+>`4h<9x=bEhRd@oZE~{5&d4hOT~e4y z)2YgzgSmZ$HuN!#A6b$Ag-*KC^CuRv`)oHRzzLWXhvc%|=QZ`eyR}bl)Vxh?+Y9(I zwIB0k6|ed;YUOwY`F_MVSBDAX3HDsM(?{eZ)c}qUm9OZAqQ)KXIh-u9scb|0B`lQ+8)GJ9U=vQhS<_h$pcXD+<=8+BH$;5hd3A;95E*vSUmW}&NU2;u7a zng({lWZ%;UU)D)k0Izf0v_W3&?52TAxRCE z*tVc#J@f8GvOHs<-`Z?8_du+cVJ1th3H`}i!%55MvX-ST3p+r46pFktQ1Wk4aGP#C z*H9B+pvP|cg>hRY(1o_q+k(KAvnX991L8Yn9Xf6sn0RyHrXUsNs-8xP;FgBoa>u+g zvJ5EwCtwT)ia3K^bTC(s5vNOfbGCmP-cfFW2zo*|iVyCY;(v$xMDL@TwH~kaigR)l zEY@MAfm`1QN?|kaptf%OK0B1hY@i&B9=PKY!BNrvbD61R5O~8QflZY`R zy>{hdi+xPvnVfCQHC2Y~E9cOojBgZnBhqzjVPA|fDvSk{45fWG)$EAWaWHU%0f%A& zRsoCzIr(jIW=e7|tvlDh0Z^AN5DB$pqMABS7C~69UOz?zukxIiu(dZtz^&1;~Anf>~=GLbC)R1VMsuRVPcy1>;Q56k<$m0w2%M3qTf0l ztFH$H+OD6lqmJNh8mq`J{mLtVrKIBrmumCd;Qq5Fn??6vZg4WKfyMZ02+g)YK7k@9 zG-E>R_QA+%3VJjuU0Se`o)N2nud1?g-C-&n+@0*pJ;=?sO!JhGHEnXj&4j3t#s|p5 zh8$;_g`9RS+wD_nMoXj78#&6&KST0Qv?9$n_JwMrFF{IeaKAKk>}Hi?0g>`DDB(Xl zAU5MXe-p?Au~>0z$ucvEZuCaSQ7&R~=+n9fXh=pS9(W{A(kQ0`E#cu zKn)&MFDM%YA0&qpy|eW1Rka$AQOmHg?}HVZp2+o_(vfJPy$X!}1`k&Y?5hF8e8a%N z-3tI}&49b~Ng3tw0L4`ufp0@OsTz+yssT`TbLW-C6ciju~B<%zb1Mer8XI)Khh zq-9sA-va^WgTm|XhTB1O)cf;F{9C<6Gan0}1*T#ge_FlYi%0AX7sNVY1jmUHRPjsz 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

c#lk-Jfw0L@SK^1mS9LgtXov!` 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 literal 0 HcmV?d00001 diff --git a/build/darwin/Info.plist b/build/darwin/Info.plist new file mode 100644 index 0000000..4de2887 --- /dev/null +++ b/build/darwin/Info.plist @@ -0,0 +1,63 @@ + + + + CFBundlePackageType + APPL + CFBundleName + {{.Info.ProductName}} + CFBundleExecutable + {{.OutputFilename}} + CFBundleIdentifier + com.wails.{{safeBundleID .Name}} + CFBundleVersion + {{.Info.ProductVersion}} + CFBundleGetInfoString + {{.Info.Comments}} + CFBundleShortVersionString + {{.Info.ProductVersion}} + CFBundleIconFile + iconfile + LSMinimumSystemVersion + 10.13.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + {{.Info.Copyright}} + {{if .Info.FileAssociations}} + CFBundleDocumentTypes + + {{range .Info.FileAssociations}} + + CFBundleTypeExtensions + + {{.Ext}} + + CFBundleTypeName + {{.Name}} + CFBundleTypeRole + {{.Role}} + CFBundleTypeIconFile + {{.IconName}} + + {{end}} + + {{end}} + {{if .Info.Protocols}} + CFBundleURLTypes + + {{range .Info.Protocols}} + + CFBundleURLName + com.wails.{{.Scheme}} + CFBundleURLSchemes + + {{.Scheme}} + + CFBundleTypeRole + {{.Role}} + + {{end}} + + {{end}} + + diff --git a/build/windows/icon.ico b/build/windows/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..bd3b4b193a0532512759e9df5b9cff599531e811 GIT binary patch literal 46072 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 z zb;JoJ2Y>Ks9QwsqIX~ZK4-?DRFr}mLT$gtQA$`QQj_Wcbh^|4VtDi-A|9#l~&u@)m z6PBxyM>3nuAm3N8&7hV`37pvt(5(`NH#3veTI@YG4+D*9(M#$MIAd{>!M?^T9+&V#HJ=G zd4693GE42_(PV16Qa7@J=Wywy_Q$%W1rq^zG`T@k61buWWxUx%!c~?lC7>z2LpCRU zG6C#Jjx}bKUnbd_HQBUrrBOk=lBov}_XOgO-PGAD{+bAY5G2#epDo>*!!6?YgMk-Y zHCFMo#3bT=MeNH+OxY!$M%$Z^fE8e?6CI|%H@D*e0{W2Ff@Pac$xbqE`4U(uKof0y z_r$V2PBEj6o^4#AfjmXZ-#q*b^+pu+*hJczN%V{-f!jzrzlIG@#-IL=w|PcEE7b;Fch ztVWQt0Ihh6gOHCR4CCtmXm6V9TMlrGZItB-@klw)6<2BBgjH&c&#$%^i6Kh?2|m(@ zX#U=`6raxH`Yni_36^#SX}cRk?oKYX52Hv`13-m%Fumxl11wfgPygi6C!{gEsjacD zmJF!UCT1ZWy^1|PM9Vh9X$4LsSZU3f>~hTIocUb;$&J{x=J?RHvJpm$N@$L*%{UJ* zeQdYpK%KeYww3iF(o}9&6q2tNHMc?MkZimi$_gKi{z1$CELilR>y4{SWs0)t$t1mN zjQ<+2%gsK`E z?Hc_b@ey@zP+bJD6|)8E%ui2^N`vn>LYSy{EaJR(FK>e>eA(51rq}MnP1bK*tj~-yTk5RC zO8Nyv6Nfakkz-}OZ&+pjjkJJ7)OX|n*PjhRgrF4uH_=3;T!twgf$ZPVzI1$bpm#>R zKOy0&GC@0`XjidDaKrjf9G=>-waFjWP#0Q(%miEy^P4}<|G41G%aw8>cm1hKm&c*U z55!rvP1jk4Wd~yYA&YC-zaHVChjg`C6{SEw8W#G_J-_c@8QbdFiI`Cb*uKl=Haf2J z)=B_WY60}v<3{$4Ku{?c`IVXDEH}lk4%AmZx$ZNN0~=t*_n}g$a3#Hv;<^hyc(4e< zX`5Bsxw*v_C=vyCU7P>=;hcO|Eiz$lXq_Ya*C9Ie5W5%Wold<93kRIOK}ZmA)}P9( zzdX7xR8ly{s^;c%KQr~Z1ecvoZwu@;*aAs$f_wiwx1*3z&owZiKm0TYnSO+aciXUW z5;9(eWdNIgMnBZZGaz(;+Tq8dP-|5R^aCSq{-Skj50~1p{scP4`S^shY#dWcyt-J6 z_JtZw^~k=+RHmRtPGMIg69GY8BrLxo{FE33agwVVeg^MuUOT);S3)hTp)N`xUO|c< zi}M*)=&UiUTed*Gayz1X{xo;K>*6O$ikv4AdBevm@-#(99wlEh z;gcYO3TmT_e2R0(Vu+Vq@VU}|&cymtSDGDe+XUx%I}zUb#{A~IGT%^A zHq6>qz3W7kgqrTYHhXbKim&>vI4;sU^z#Kr4js=KCwhE847G6M)+sA6i=>x*7hgHco&RXXr?L=UU~i(O5aWvY&VykQ>IdK;a^_spV%kG&3RY+w^Cqq zHsinPA+m9v=>S}398zCqm8I=%UEROqc zy?FcB_*{`sFfS~Sr%-O=6nNZwZRQ0^;=Nf{UL0@18hM#4RBlMhY(Y>okt%U{x2~Yf zzw`b*^|-<+fElIjI7ZGKQto&t+804{NH?51s)K5oNJCNogksG+5DD?O=X|#OrL?j> z*IPfCWhl%RNDd%vg7fNAGH-wo*JT}X4!sdo$vBP-9WN_HrccUDR!}qNdRw(sNf3i| z;?x~FR7^?>rdBq(I;Ep(*#u$a&~m3#^HR+`qNMm}ZM^*1bGBoO5Wi)6eN&Y=!;|L2 zY0CS}7If(BLQ<@^1K7M7 zdZqx*Dy#w)3jROIBoB-mVgLXD0000buWIZ>fYOF@&*MYM3g`hNdZK%BopND z8T^b_&j#a162=(&VVeifakaLe17Hx~zexgNu(8mB!vkZ&Gm;D#gou(>LRnf#tBtc0 zJKcM${@*!Ob-TK!d!~D)XLfqJe$w6U>2Oom$qB&2!^6YF!^6YF!^6YF!($l0V2Ggb zdew|(IvFuUOffJ@HVsbJS$$+nmX8F&V;dpGba@rT-Xm;u}<*&gUf>!C5U)4-`WFU@cY;DM;=a;ki4;{^vK2w0`ya-4_Q(Zr`bP?cFN}jTsCK z86frEaOK6~k`|(${_yi3E9>d`sbW8&lsZo;b#^HD_IxBDKrkZkYAm6S6t9$2N+~Vr z&A*&8&bg<4k7c-kQfZpn6@p8M)(_*1g@W;1ARr7h+00jxlreS}2la14uz#M~aoSCG zoua^ky}LMEzPYsmwy6oHAcz9sLO>V>nxhL&8~vsp9xXsIA*F&e!R5ptO%i&Z;A5OXoFqz-ElGTV3Bg4^ z;*bvoRO%_DQr*q~z8DDgsj1(%>6WIoyh$0sregqfv-a&(R)sqHf#kX`8^fDCPt!L>%TTY04RVO#=M=kz=P{d+wgS_07r%*rsyq zT?s0a_#b%AOO#^&O9=LoD4?4P2@*YW-Z0?#LY_tW9OUwmCisld87L+IaN{8W7XTvE z2VsH!+ry(1lmw8*q)%f2l>*2ZK&HSHzOJ&hhT1~CTBogAsX|<@Bj=Eu^I#+|$bV3& zr?)A|-g|g@_V3T#b2~AD>#o}+E_Y1Yp=JQqRAO~ZUWHf<&lYkmpW4Kaoo02NPJ2S}>) zHv{cdM(v=6jG$bBO1YwBlHwas%mpGBT1KGWdw6<*7=eyt0i0-O;R$5`s1jMi-~X-) z&do*qow-2#G~WNYN+Ok%DhNYSDCVI!T7obNNQsy9BSipVL2p*lTi#4Rmf_s^96321 zFxAgbINS5slu#%$P)iK-A9h{g-&U)%%z@TY0+3YnmezUxnTyVUE&r2 zh+KmqSRoTH0>outs2QL`Q2|sIE3i1X0JU0OGGMBh7Xm|#Nvf``*3;dmz3t|^DD&{j zJ(>Bt(O10jnR_NJ_mOvBcv%$k_mAYonbYM2WFk{8xIm&Gofrin4B_L=4o4c&9?u#j zd|CsDHTNaL#~&iQGW?Ji^&WkKa?D3uTf_`-T^6+YNm_zgmuMc;=jo9%t+AHp$}m4W z2T77jG#MsKA+Ofc*;KJVI_ZCY`va{S@kU`oDa5_NgI7I!cPRLk_#^PgYJ2YYiNKV`OzSrsZMjuc^Ra-8gZq;rVI08r|qN`+h$Z(e;4 zAg-E%Uf#x5^YA#)V9lBVng_X2trb`y7B2u)XMiaba-6h*g_(Jfpps$~;DS%;iF(Va zzx|yLwtuFH=ft%$ci^&S5;k5Pj4>Se*!JGtk;doK9MR4KaR=t7*PspR=Wj!o^H0;L<>9A>9yVPTFOhRL>47|d1D zn`_m|WoP`}t;dzx%~6fNt5gP<(Wi+&HpIV89~u)T77lvi_ZTiP^mWC0?IYo%9L|ja6t;s* z6VUWBfv*O~7n69r7+e^H(MO5!&)LPaf2}|08`V+ll0tpwT~R9V_a0k_lQd1WhjiwYCw`A%12cInm19~1M$-U&1BLB?MX#6*J5EeMg-7P=$yhOb1!~ol zAApTU`gI!p*ojSCxc{BcdTucazL`p;>T$|O#!4(x`*UN!Q02jkuFZj}99>$i=ixDo zkSQ>X5V-VFJ_v zVg3u!2blxbCy>I})HoDJi!4bck0q!=2%kE5)ww5OcOlzXV6|A&mq`u5{Qkc>|H_Gy zcnPNW3eE&NFgVA0>RdzZuUpBVeS0m3_VC!EOkfsN^_cEFgiIhe3Q8t0J~a*~SgZ9! zmU7~xB+h?|NC;;vrB z)U!uwvHX4%uqQ6o5?Lx0`Q-KqP*MUboum^UWDIKIfbsTw;`i|An#f!lnm<5V^}^gD z%uLU!AmB3OT*gXX^qAlI-uG9VDY!Zl0MmT;fLy4>@0ciwCzfkT5(a`wda;h5a{Dp8 z7c>4)?Z+mIp7=dHx(7BIBHF2w7L6JK`U5aW02|`Qv4kjKp-kZ_@&oMMvnm4XnE?=| zxqNT>@LyeURw3YTn^}yp?-o_|1z7$payq1es!MG~SM!n6&o)r-o8SBfD{dGbj5QNL zXF66BqEA4jFg`g7LBPdqIZh{v;)VN1&VMz#98x<(rx#%N!`^V^9(`_*zCRZPQFSp+ z3#9_X#+@mS;2PZq3hSTii=ZkqIH|+;<@4}Z6$Gey8Gz3KD@+2*PlBvhAP7?^jg(++ zW{xdZQx!1vmkgJ*HnT)=^u$pX3L2bfe<5OBOo!AF&fW8 ze!8sJsQxGV{`t*s1HCCtOIR>hTOjWs%a4;=AYaS__SdM@QaN4-wxuljO;Z1R&;oeR z(mzdiJ2wF0}yOi}Zg8rGHLbGFwTc2!%lPpN^C_?YyXg=YsM255#zM503$WX?;&^ z#%BQ-W(kgxPau~MwH8oM<#;jJmL}>qX!z)r7mNNy0rr~5RK79H2LRPX<_md_(qHmO zH#NX^-lY257RlzsJv=o@QQa+t^S9{Q>nkD3l5~oRn4SQZXifb^l*q^iKHQ z8>`(!1Xg4KdMCP{KjZS}<|~O}I10H`q>;%CPlg%kkDD#@@wD&Z(Pv->tprqk5m+Gy zEF1wzft_V(Kku4D%A71MtAfV^V#=}(FTd)8t#Q6dOLl#6t=9~LU$=ZGU=Mljz5 zVH3akZR>9eo$WY&I4IC4y+8=ah%^d34p4-b!Z1x5sLR30YVVeU;(pu`|F0}y5=(JuYr8|4~I zfE(p?yFf^QSJZKW4=5Hzp*{o_C!qSDECM8M)}M!m$NB;TeF>$XBBx7)-lOLch127+!N8k7CvBkyeS+!W=G+!

0BH{=ZqejnB;^1&lOO`Y;(A>cqhO?*q-O!Z5pB{h%?j*b z0_-N8exX`L@euE9%P^4pOFmBj5M4*Wd z5QONc2N{JNHTY*O=LcwS0_?q=J}eYGAqrSfsml~=|Iz-Zc3+t9kBvT&`}piX504WE zONUq-ggF7AuwS6*CZr%$fEqwYv}XW@TcnbmgAPE{_JcqWtz*^^u^BwDZ}q*kfPB0q zoueDp-n0T*mqY6g{Br3ZF!YX^cv=GnB>>XQ8JYmV6k|z3dcsqeO9*Pu0M6N|e>MdZ zWL}`HfZ;y`IsCK-GAmK^*fOAAE=yk}6*J=w*@svwzo0*8MRZygURmrvkQNcBo#O-k zR*W%;O?nlK;smeW7=WoFQcRwoPzE5dAPDqt7honJIsj`X>Ci_2whG)|(MkZ#<8fqF zS%-XsJTQy`nQqVcU&Fb~+V0FNiR%U94IpQH3QS;>4mALV1wpKur0C>3^N!r1@w0y) zj8twipLek;NKdC7jHZ+qV#WE za184~aBu}fZ9p$b%@j09LvXT0Ju_Gjw4p^D&55}}ppqRz)jNH22?H>_#MJ(=iwdf` zcv_qi+hAPi5FnWVxLPm^(DQ&T0j=)Y=UVC$DJumMLrW(;1H{g!f;jymBK+jY9gsZq z9Y_xR0OaxevyQb)1i)}G3_db$h~0FEBBnt8B!XuIeFEN3%+s;WL$P%@q`-xMB#Bid zcyXZ?oCW}Qz?B%?+q1F;fNH}2zn&`qh|_dN5n~+L3FR0C`e3LIW@q3r&}bokT2HvH z(mIWH=i{eoyC1h82f-=NqzxhG4?%kLPDl>k2I=A3LC!n?5=UI8F|HfU41ydmvQMpM zpc#Q@0(}5E7Iaa8rp=JQ*=;&ToCV;97{I%kSlczNb>AvPM z=*Wmall&agdKH3GpGXZT=}i-6vJ#yZ+vrKOg^&LUy`Rc)NRQtS>CwAD9=nHzTBgVD z29{LFcfiOHZD<3Y39J>ZwAf6e$jmucai?!D>jPjCJqisK(f;mD&piejTHS9f7VPqj z)*Ui}Bu*fA>UrjavJnAtsg@RkaWJFWUzmgdH~T%(5*~%}7v2hT{t@8lKqTj3BjV_J zKp(IiT_PR#Z8o6C)@1}H!bcXKOhJkQM5kS-KhQ~3)9X&t8M^|lv2PN`QYss#hSijQ zN=0Q9PWBw&L}CR7;12j$E-BGtuwhcUQTikClZ&SUY}=C{*#0E_5!>x~B@jbEEkQGZ z>V5wSi#NU#pnL>^s7T}EsdFU}zBdKdj+N*VAJhJg2lv=|>VGEvm1P19;#w7QkG)7c z@6f^23+VF<=DzXwm)r>2EOE< zpdEXw70*0B22zC}Sp+OifN`$Xe{vu)vIsI$z+-J#C5?IwV5reh{q-hEIwv@Te%liu zcgi`vss9${^SQ@715`8$Xo+GW~RXxvMdA4VVV8;rOW#Og#>gL;HY>k`77X@tPXQ`ew3g!*X=FGJSSt z0669k+x~kKV9Trj=$2|0S8_ z9&3X)0eXX0eFtD$uZwrne#!BN$v+H|S{b5~&xYuvv&@j!)h!XM4M2=M=_f$(Nc$B` zh|yk{=K-7C1ZZX?t$z)!8qh3#bYe?pm7Y-1gy&+5Y)$fwA(VcG$+BOLh@OVLWefoA z^|7bCm~#1bzX!3(OVNxYf9kmq?szgJ)AxWV6hV6d7(LqZA&<47Jzcu5V&C9S7_LO1 z7O6vptk_P82Ah~pN4l2U$#^rAw``I7Do?;o-O6-ve4gs+I_$ojRn(c&=qDp^4o&ud zQU_L?f|0XcVh+}=^l$IU$Cy9X0*pT8#V~!tZJ-Lq3~(>mM*&VKx@Q1ezz1N7C_8m* zrBs*%zVmcY)j5#$MNl|w0f(KY*lP`oERu#k?tVMF_D(mU9piA)Xo=AXA`tB4qDyNd zU||7xew?Pi%ISN75o2=FvDTV2<5c?TqD~C|p~T=2R~Di0l%IlN>|~Sr+bsiN%bEdW z=l(3r-1G^MNen6wjoWE54}B6)AFxW=TtyUMFu6bz@l&lk1DRAIn0OqNUj8?F?s^() z1`wA)B~?&y)jU?H3|0|iFtZS7AcGE^qg2yfatwi^^hUx(?>W}WV-XE{)I~(?o2j7& zHrAl$d8)8O!e=2)coFnGzU#|S{?h*jUKls6*E7SQgS_qXJ~+=ZrIGUgwQ4^4%wK5S zu;mV7I8lA>2pqrtdN}E#%jrIl>0m)kLnMTgr$OmS7enRtFF-gtskMNXbMic3wOGkZ zzuG9k7DOibYzZ|8NEVN1DQ^fBi4S1v1fHb+!IES5P(D88%%Hc{5FG0Z#Wb9M+7G}W zsV+e7G0%j;@n#-VMc*pS;v`Fj_$V zG0%YF6JH3md#{I}G+}%JN{0oxt_kqy1KKG7$uNfjUD`EvFQvUI1=#K|u2(?DWkY=p z6~5sR)V4ZgD*YNZyPu|g8>eIDaMXm5R%WRvhwO9ApvxLCnSWQsjFiI!ZT267xUvAz z>E}W5%uBnI{-zr=O807^|z5cd)v>Vx*K&Q4E^3Q7BP01?~7Y;X$V^H-9Y#wM0AW0HPP(@FTm)=GQjR}K52C)*FJnlB! zXB&T@sj)Z*`6pcp#V0&p-=KB`?CuW_$~YYT;TK{4fgga#6+uJ=Sa@U~{Y|Al9jgZw zOuX>T0Qm{*S4Soe6(2b*Ch3~opo_=4f-jc|YH$R0VWBkpa7E>|7LgRM(w;f$hZa% z|Gy6b4|8-vDD`np6C24s3o7n`#> z8mhkp$ppmZ85q6bl@RTCvT^*aApK3y{_xG8gT+T~g`hA3jOkp$urLDCw|y1r^GCE9 z(C%94bqQn7{uPKG^K?k6^ZHk^W{ICN3G1_U_5yBc6QBX7{M1St+xrd)jAHuq=8Ahl zJDI|TtJu1WbLr3M+<7-w-~5*Pak))ql*dm@ZK_YK`8{g|l|X=NTB z`Pzpe$d!n};>0MtEYt?#xr1=@yZ>xH-p)QTJ%NK9OugjyAXSXC9BrO8uGJjQW7CBJ z+ija2R(*DDOREZ7+tV1+_|{4!diXn^h1ztZxn-8N-}1TUU?QV*yb&;N<7O8aO63wYyFeX2hI>vFBa^^Z)sN;Nv@hVfr(kBPt0_`m@gs z=n|`>(H;_k2Am6IdVoP*W7e5&xep*J7EAPD=FkTJ*} z3~Bf4n3pdb8>+5Uy@yiLY+BE@pTWPiSzBHl3LC22Ut(GxghKFT1dL2C$|7@z4lTzJwYFm~}f^H1dZ_v$p?~e&!et@PB&C_gh9R(s^fa;OE;m|if38%j3&7FM#4igxA?&~2rz8@+-ycVLd z9pqrdN+Bv#&I*Pw64B#r-11OOtD|{kuS}mknE;!c)HmiDbeRCxl3wtpEpRq^4T(f( ztzfxIxqhLcF`J*?6#G!ZQ@sY0FZ}}$rET36;Ty+aeeN(k{FV1XkRPS6pO*Mm^|#mQ zngWc>pr9}Y2mkY9P(OY^7Z@$)Xf^o(lRx!GkbClrAf7)4l4=H=x`mc&^ym*($o$yC zo10711CWqW1rAW{#Ye#)6dXc&G#lp-SfXHt5%kIgI-P5W%?)!l8hk39@Uw6nO6R>2 zif8_0*Ij-|ZEA%TlhL zJS|}Ar%zh%f`kRLNfwO_<^&9 z0_Zg%jGeqH1;9PO&gX9ngmk8#2_Kqbr=&6u;-sg-#HGL8b(bHj`X9gJdN}&s&q6dZ zLC#Ls$I&2u``kR@y%DGdq~tI>`n`XGxx2qb`|Icsv^Q2M;86*-zvBHMPI)@SILF8O>i{lTO8!x^D%mE?@ZU2+x&)!F- zVVs5iJdrj9KMmC%?Pgu+H~y1vgWM_Sc9rX|_oHq9hrak8s2;o%!hDGsfK~4U=eqHl zNaeSW<}>y=$K;^G2rTaZ5ghp1hjbH>m6EP36DUr?w#z>R;bSj?I{E~_FoCR>fpguh zN|MKV_yE)v*;+2l&9ChO)Sinr?VTpo*Q&qfx%;~PY|l-FTT8v#m4BJ%w!lZH`kF$Y zg=3IE`)6VFf>(8K_Z!uJ_O5Tj!LMHf(a0n)m6+?ZHox}SW!DMK*KFUj&#ftN;6FYH zix1tRO@VHiKzFwv0e<_boZst0mbg`|J)ye z5MoJggAM)Z>up>sZ7yRF&gbcG0Zhhl|K2~OQsPXYbio9$N8{8lTm{7oUqj{qCJWJA zfh;=E*w0G4^jHed09FB;*hBGYQtkCZx4AwX|F)V!zg7R1)t+_wq3k(E<;^MD{V#hb z+0sO9U2q?(`ak&jzl6%cyCBG8sjrdzS=_&&`e)Z}OaQF`G|T{`1T7?mtRIGIHMK-B0zUGG7k$*@JN3 z-amvOmj}+UAg4`uzPTFrIaYc;QDrhxgp_77;&fwY7$f_txhI~`B=AnZaDz@3|n z!Q2_ohHbC@2Z)~h5~wd6gA^kjroW)xWOLJ(6TaruCfJ6)A;{K1Ta5s1VkG{>;}AaX zc`*6X-|wooUt^!RQwH~4_XpI_BZvwlO*7TssQ*^=Cw~SKvl9^^Gr$fza-svR`#{Em z`$yux=hJU%IP$t>ki;-j5FJ{&YvCFQ6l!esU2*aJ0?YbrbJ{bf}^+aKKGXYz- zZqezx{XWhz`^-!MX`Oy$V55(LZ7+Y9Q*zve-EUOzNB-l(aO_84gdBPO(=2<@hV+_L ze>2XHJRkU|TK>*_A2u4`<_B=vf37qMGyDDn_J97bOoC9SCZcVCH3jAdl+J%8Z2Oge zfbj8`5Ccr>bxIt4vKXe~j)xRm@&#~(%{JGS`v8DjMZ=(WD z=7X)CM23+-G`bBQ`Rd=mk(>XC+NJfz1dP3b<;2^c2ve7T5XOG?FF_Tz>zsi!r8BW6 zfpqWT8d|Hvz;2!4O@K`f)z+b@EBtVQC$%z6{oG$b?lDjAI@t$zgdYE7aR%=Bhu<}6 z%|fg0j>=9YzFDcWGrz>qE?j4uHV2Frkc9zF)}&(&P=OQ!2#cd|@3rrMx%SboWVHZw6TKu{i@ZQh-Z>S!=m$LnIlMlCe$>cUPh5Xv>Yw2s+E3f?mt{IUP zN7-HqI1w2n-1YG{LG{@F?vjL-9L#VgnNrwu^pf9!sb9Soa?kwldh*l4ajg*$BQPC^ zT`##BZbsmJ0KNYBnFv|`I z07-eKyL91pAm*pTg~7JR!}w4C2~7UVry=*uUm{0gGCvJzy`~$E(C`wSM39Asx3Yg= zx!czL9ah76ht_m2+I_a$Bk0d&z5I>+kKw-h{81=8{TE>JCGXIq!@KqP8{(h2>zlCu z-`)k$_;xMzal99r076iGKNA@L=cEBx&tFT>&lDatHam0Be#VK3n%W7QNM!m41VWD$ zLsMX63d#q53_t$Zn?NRY(?w_%Ou&Wr_lRBZtu zsQ^o=z*2m)&vA=EVS6s;BHFbgYrSlGG~AUk1et(g|a;@D_H#<7XXZP=3_SMR7I~}i^&vxELmdz$G%WoR;k9`)L^viz>JXh*TI%?HF zfO|jpM=*284Y2*0KMmWS`Lip1i6#J}O|5+qYxtc>Amaof)HK3not1?hkB6K8_#zOw z5jg8tujXutLkt3C<>3w(Ma{)CACz>S}P1J{3y8gAm0A{}yTUg?>NZ%Q6sn$!XT9bFjg;Zlm;+6RRYd&Qe02JYSCUqO!$~bWYMMnbB=rQt z!vI>9fK3k4T6sYE?jw)808V@LM?sV(S0(vp?)Xo*Z|@&NSegJCS9I21S0O{yzkAk1 zOj_J7of;;z?JrXrJ$C5Zd#QZ*DX;rmQqyh4;#zU!-~I&R#Tn{TAX5x~YUVdkf`4OX z1GWj6wr-o9nyi$`PPMgZ!YYmP*2W76V#_3l% zd>{Pyn%@A9-hGtnJ)MV};DVphZe!!v+Q2$jTe`p^9VJn3~Gb-exE62A$*NeOp;;w`ZF@U0LQ#xMl1e$7ex z44ZX`c>|~kV6xKUL$|^$@A(Cof8d9vcA$GEU|XCY`1f~1edZv5h#HMll%u2#!p-S3 zhBFfo+C1+Ct36H+2WZ&3y+tUr>d)bxYkv=p{pd>&jcg+#$|e|XayvImA+nv8XwXOj zN;0+03Sn^^;+cbR>-%=Wk?((jm_Rl}unX0H{+@5ak(>4cFN}jqlVwJYsT03G`@Qv} zC!pGL|LJS$7500*9;cUMu^@u`|7j20bc!{NY z%&+N+1DMsbrpA=3^BLKqBWRlAwu-D&?As)KvT9~`{CMb-XiO8VSoS7T-G}qeN>TIjSGk{GBOqUHu zcfjFqe}>EfWCFCHQG~ZJ0lWAQf9G>>-)H^+qS7Q4;@DDNyQuYW5`0ap=t(CvS>lGH z&E5Ijs{=(K@VHn3C<1NbxIqS^i}J9LHB)tC^4u^1J?;!38s7m&zyGgLe)LW_^VRQx zu_s*Mq#p7{$#Z!Y9{I`#XkxK;2I|5j`WQ3_eo06Yh6CMa0ye2|0~`iys#xnzwQk;D zn`>{~MN{anwJT}&$3bUcG_nH@e&g>UC{DoRe{s)}NxPQ#kN)uA;qFhq6@q**8`Di4 zd9&%oIwx0QN8Z{Z%35zcms+ z))+*8YniJgT;wn6R4 zeQ@hlyWotMy#r2r@tgELG1?6>oP#%BLnY6J@f}nLXdR(h*Ji-u=#=BuRE5pHv^Clo z{jIdRj1$0X0Gk{#jVUSS;jjFE5KWu{k9qN%vaC6J6ah@%_8)NP$A6s~Zc>#U7Qon! zzSfSm%6={n@$`dm@71q`6pL{v)z?(`Da$FemZ7DzqaUvphVhYYij++1JZdwIhN@V7 z7=$7948-(Y>Onvw*fr=g*a3-3j1@5Kf{dlV`4Q5V?z`>}VfL>7f+t@7r;y)%1|0)K zsRzID35beg)OWy|7EQAoSfkCmDP9~J>5uRXV3R}7%#>6zi;ltk&d0;Hr(Xs#sR0ji zaQyb`;m)hC&||vMKDHyg%~*1*^SA0P5xG>raG+ml90W;Hq)Ah)XKN2!t%**e<)pfm zjPCawSA$mVt!84)6f;-Ab|kj1J71^5C>&m5HvCSC(~Pi!aeN0Hzx69{%l;q2887=i zIO%!642Qn)Nl0c6Kz@8DaC9PO$8a`isQ=l~hKK_UUnXE{hSXY20Hhw?&IIVgaa?-A z9b&_dC=d61>a9@toqvVmDQCj;tzU&ZKlW?%IU3@Ly>YXF9Cj|Dm2$cpj-=SS>5}P~ zCrDg$7}tl~i4aypAWh7`v{5$$RG7j?+dj+;8F5>X8{lc9-e9G4Y94h1&;)X$6Ci8z zaPQy06=wH+6)KP30a0O86F+Y^woMuB2s;Ssvf#l(7txw!;5C3viiXqPE_Ve%n4?~I zIAZIh=l&Ah`{}po#T-VY;%dp?XvCSd^0pd)GPUm*A29Ybw$CIfoUNAz3+SgA?C#4- z+AQ4HG*#H0Fx1Kb^uSSRTZ$P?0G)!mN1_A~MG!`Ln7!>QG~*zMqU>JiA+F9SGuB`x z*?A6;^hbCG&UDrdn3?OT7Zsr!S*3&ijW)M??JJII~iGVBl z09v;#<^#wUd@td-*8Ozh16W14k?eZAP#q#}_|c54wsZUJ=q-}`&SH0TTz#tA=9Rpe z@W}^YBLSVxsVBlPT1!U;(319Rh#s%Q60UyDp0RdPk_!VB7@AIGv=@BM3`cF~F!})5 z$Z~gDaW=jU%s><4lW3))f%XA(Cw;sLut{OZij&IfWUJYx9q!*YWR|twb{D#n*{1oO zmY}##@?ko*zlFPI`Xvn8twZbF%&yNFx$f33Waoity;Qnuv;d>Qko3nK2jeHOpSMiK zwm7%iK!d5Wp;GtT!83qO3QO3H(c4QGtzF=)3*Acp)?WUWG<0ZDd%lS&;LBZ0~jaG<>&%0g&K(`^NXHVq@N8G?%Gx_&pm(cPb&O5~1lXU^w_6+L7EqK<`nV~dJA z&?dlY!gFpWp=(C89CL}O(c4k%0?qpL+n9Og7jP!=xRZHp3b4uia~Hb31(bDbPAU5Z zHCO23#2ONIg}c<{{+bEVx;wM;CMBWa#Bi^H7>u#$?a$Dvy)>ABy{8S;9|kh)*9yB9 zTg=lgZ2+$UY+~59{MIABgb6sr*s3~bueU9-DR>63Ns+1hO^GqaC*Y>9HaXI;?H4q0eI1=J4!)NC4}<_D zso$a|WSIoI_=hr>ZA-5fif+9}_IaC+4kcF67Q8P(t_?<~>u7zi+kJI&{cAgpPS`Qi zc2TXTOgS_Mj0$UHl9k#7zW64_?0Q^)qXJ@`*!4GTRpz5g1=Tm$u4~>0B(jZUgW=$Ha3DcjBn$U10nh#ihqWW}wClt%k4Z&Xn*zh zXzBj!=H8o5pJ}qo)&|$JZJ*O#u_!1%4#CvpK~-m{nvYt4Q(ymtDh6qIC|1eY@mEeQ ze&cEFSEj^c3w7^4l1n!r1%({&)_36g;mdt zI@{7mmjcsZ4*c8pBEY}xbJ}nenSIp6j*rw65s6;pfQ5OzVN=hUhhh4oFKd!hjfAOo z=e6`VgaKK@y`q8u>IH-CBSX9u&6OF)DMq0*czh#;( ztebw~t2$}O+xXpMcryXB*H$H;mxdEtkJ&aWtB2#b->0m<_F0E|$~6NZwZ951%A4c3 z!N1mX@9}ci^c$~|3Y$y==S7-^EI?N0sjolaq_I}peyE_@tVh*#F0j`C1_Vvhcn@paB4(&I_dwHx3KNnzt;L2g0 z!>{Z3kp|MFPGi58esXIftt~<#WrGP?rRhYU*nnXL4FH`#Hf5|u?H?!;z*!w@%>-_V8)|edSvYRXXPSx*+>)HSybOzBG<$kmlBhzO&f0G);AvV0|A@8(Ka< zF_citzdgJRU=7F!(AJ-~ref2F`V(?o_8g2BfZcu4GzE#FKKtet2QoWL^px4)@L>1R zV|jG#1Z-Wz1EBn`OCEs!P9sK7wdv*!t8PlHw>LfbbeXL!LFvc2>oeBG-*5zChg=zJ z2H?g3I_}#f1#Kqie1Vl@lD@Jut`Xy-Yo}~UbH6EnqMJLgwXj;o*ZW+6feZt#-~;F- zI+r9M(czZFkaAs)nsS|PYnPnvw2QX60-2(OX4J*EH-c*))@iyNV+K$|4?I?4G=MI! zni1QZ3G^G7K_R+pnqjtT*vMbCxBa^maXt*#BY0s9L}?0mag=iOo7(nR7{-FV>shKm zCHRQ&-w!|(N=r18C6}`nic(#?#013iv+&fy2r&Sf0k{GKz%*uSdZF|uW?&;S zo&$7^A(;zco@C)T$5q_p>El?dje+fO(Z2VfE|P1!uQh;WrsTFDEKNZ)z7qt_%p;G* z(h9+T(vdy&EMRJHKb}9<1YmVzSm=I#)`!uw76-^V)i-ns|Fztg4)7X4&sal@udAT6 zkk+Ppbm%A0SFlbD8G(KbJeMAPB=%ZeX5!I1JOk(% z>#SmC^xs{C)}^Oj2&mCV(A3d6Ww`5^Jkuapr;T^Z0F&-^D2azm^eThjH2c0G&5RNR0H)dil5bP*)$A9!98^Ywv9> zbY=jv08s365dB1;$x}8s=TA;Bo#oPbjl1rQ9wnk4jolc&;SLeO=h=Dm;ac2N$46oE z7sz)<+2*Uz0L;#sOQTyJhY4z#ZMD^a%hAjxr=JY&dCP&28%Dm1K%Zx8_g?2yMw9N@ zEup_>$8T{nQMkFyxc*2V)c~iXFNQ7kI5IQa4~-8!1FZMmHf^^3*=GD*H(tN(hDN;) zpjWKL_UwV6Sb(Fyx_9dJbQ*4_VLMTXcb+HEucJ0{uS^LuCGR)R$4BordsEOs4g&KZ@hrwX|PNtzG0_6u`wIZh|W z6kg2~5Oz9HFZ)~C+{N@x87ipuwArN&FgzMSpB6@1T`&nZy&J3|H$u~Vy8#dL>&9Bi z$jGJ+*@2?w(e(xNG2&q-598V-!B4ioHa*BB$QmAcY$S$<0jw3Kxcs&cz5!<-T{zKtH54k>Qtmd-_c^e`E1u_0=VDRtx6h1X(h8(&- z_pu-AXpq+6dSGodxqKo}hj!!cdz&3?`+%$|l4*$^AGOWw4xprCn(!9R_rvFu=+d=A z-Q?p(rEEX0_dFZ)DVJVX*Y~8;eQ{s!<(71F%s&4PONs#7tkGeDa_$4kpDx#hHvxJC z#mG4)k2&Vh*|FSKWwy31{+~*wz24aHqQ^`(kuTBr=m+%bvBx^WGXO-Fm3IgVnVHNv zokFVbG_ym1;9ZSiIYtun1Y}% zW=36iP6D#;MH8p!&>`j;xD`R|u4A>oMZ532{G_{E(*3dA$4fLXx)hS{IAmPQBJCw2G_;Gy~jd0A{`6bL+83hn)x`IR3+bgUX?MKvGXZ z-pCZRXJowOoC`?i4gwRx@Q?1Ch{L+}Jywrp7yvc|$ai3|gQf;_5IrbeMQHlsbq6if zQFG84Y`TK3IT~PN-3s&E(f(Hwf+zrqBv|9;*^gh6)!%ARLevsM*mSfSR@1X$a?HwQZK zFCtMXfrSO9(PwuifK8yG9QG`Y)(Ha|P9W^^rePHl!u%-koS7Fu{rnvUkTvGe8bv@_ zhKT-(mVQA3}m$n+XYCs0>fhy$H8;)>3|!Kq38(Y z3YV9U|u> z<});U12zldz6Sx!6FbvT_dH<`SIaPV=JU<@b(xpZ)c9gpp*qlIci8$qfHtC16B4ST zN~{0bf@8h!)0O&k49yIzNkIMpGqjQ?@ebyPTZ*C4IQeq%kFf^Jq5+Lcqn2xGoJTJO zyeDEha6d_w%mf?C7??G<9$1^2zV$N!5uN%ZD$LFs@2+&pea{HaObWYF+&<6#HTJcQ z;Tv}W`T%UW&_a9r8-qETnsYk)Z91;I^-$9==+J4>E#9v|T9f_8D(P}4O#mHF$4x>w zx_hO!+sHOXjdHe%d;%P}Z&AP&*bDU1f= zErB&*OPc`hnKgoE`W>p$rxadBJ^o};YYpuobno@QihNfIW>kiSxI5|(N5JUy?A6Wgobux7pWv~?N`r5L; z4NGbx!Q7mm=8NfYowoHD5ye)Axe+4Lsnkx}#O&SdNAT!d7*Mr&9EgrUmOz@Q)3MRx zDx|5T+9bn}eajn-O+?rT#W$R6f`c%FI4ML&IryO)Qr<)0W5^NdxWAxy`qh{~fiRLFL0;V$jV69GtS*P0&HW)^7^h z{Y?Muh*r&rF-U7=*nZy6!1xm{=qe|JMaTf0A8NC&!*xR& zdQ|I~3E2AnAi%ZhO_7aG(Z#Twjp>>2o5|tL)O05|o|=D{f*ro3ul)vHN4t$`4gxA{ z$}Y0w4z!)}z~|o!bNAf_K|Z2{sD_VXgN)}arCCQW>a#UCr!Yzk04FXn%z|nQQ)wAt z0#{?U;2_DQb%i$p8fY~IGl_01(@f!w{Un(O)VsbxEJWxgHP@N~nF-)j0McIiH%hN9 zREIyPssNjZn9fGsXc*DxHpq=nQ1ekOaoyYnjrBF94FkY`cmRwY1<^xQrpS+&@MyDh zhhFh>2D-+U4g*@)b%F$scD~KH)107T=Tl)yCjA>L$fo9M24KwxWCV7sRrl)u+R->p zBb0~?Am_9;TM1Z+n1E6-01i6E@3-U}G(OpX*sjSLu~LCnY%=@0gD8wJjFCg6ltGQK2!@<`QJ#b#%@CCQT7o$i6u)PgehP@s=mfI44 zLknn31?(43YwS;RSuBON!dtZ|L+-dA^BIBNl~CiMe2n2qf!^STAZ}v;JwmGSm;3JK zM<(za=AtgwH70>OxuzA=|F0=Ux6#1Udd)N5vX1B9xbtpLJyX?~E9ls`z0MV28 zj|O1iX5?vSkyd##t$Ml4yi7%I$?V%;EueLQV^R+~^Rg&F7iWXl?G89;Z3yCOK7Bc7 z!&$SXiUU)S`5LA`Nmf}nZ#fdyuXEG@3ZtNEv%r`Me`a5CxV*=*7>7xcUuTOTV5M!bGE(zy1A*yxlXZe7=Y|>`BH^Wn-k?l}D_CTZAAiibQfMJ7L9gPm924H=6jb&-yS#H6*{Q~++ z9BYC0q)QORah=|~tW9j#A2um96ONJavle6~yxBbQxIfS#KP)ZCnK`zE&3mkXHS`4R^xEc!yEL+;(nBa|<3fI2gl0+VrI4FE zRj2oMnc|BBPP5KKOi^{M&2?}cgMB?~)Bn%*0?c+VQ;3SAaPTXC1Bbuy4^*4cuGT~k zWHI>|LV#L0L@nK!N}JWXbbsz00mghC?cAxXv?xQEx@VWK!;&q zGBRnDnsv;!&QS1J5krv)xaDA6=d}1~(Mu3P1d)iS*S_1=z!F@euYoRT(#1`>9s)BO z-GV1O0lA;1j`h3P@FjslkczYqdd&~TSJA`1x$u;Od(WO-(dG)2rQGNeI%&nG zS@>vcwNt9g(CyP3ZP1*K$wvC?$bmU;%jmLCdvt@97=UWmaAA-df@~#Zorkjfjs&3X zdP~&G$xF7QL+F!^ir<+0WBuk%yx%pt-M{;G5zV`|3{0B?hQ%2tBrP>38eugPEBPBU z85`G0lbOx;S{t%#WhjR(wX?Oc1-LkRWdOXcn9RwbIkaN;<4u5##1dbgj{WIOM)K%4 z^1lQY_F=~&nnBo58HmuDsv7n8^fB1kGf1d(s|oAkOzNLh4B`rFw)+yQ|H#-( ziZFQsoOSJ{DrbXJtorZle5jVB!a5KftcspgZ1aYh7Fj+ROH!3cP_y*evzJQiB@BSp z*lyr2*mG?Dwl`1Snd9vAx{?aL%Yvn#k{Ebc1mLM=aKk<5c7n3wXwc5PrLDeO{Snq7Xs^eziQyxDrklA05#0}&)~UgZ4gpIQabQ)c z9!jI}vQqPHE4$h=fV0g0F4{V z(*2iyzl}%Ts?ANa2RA|saGaCbYc?KX2dUWealhDddQC}D8+87g4h($&rbdD~TWb$$ zfP-ul9Dd$=AFik|-Snzk4S;StXRtrf!Dg0{XGwrcYE-a69ca<+z!a`_HMAb`koqsFnLFka`Md}SE0k9qXE5*)P_0&6_@)mR@p&0+g4>(4`mPwjUaczBQ+Y3wa$X2F zo}_pPU>v=S$ON$cR1krxFAmu(w#@}sO1e^YS*hu~=rV{w{RId0yw?s40fwM342Vr$ z;~Kr1pt%mf;}l!js~@>qeJk3EZrdsA_#KPWY0B=)a|S9^D#aOtQtAPj_|+iaT`IEQ z)@?I0O&YH`YJIzn-tY8XzrjFlD5eEE;{$*+p%9={4AN97!5Oc_s+=T3YXEzDU;yR| z{L&jCVNBf;Xk$WZKY#*Nuh1D#A9|AhJ`!-Eu^iF2WdbMod_DRAl+t0I{5mL<(FutD zKPi>U3BbazhvAgt2aSa&dt(6mGL7;E{1@FyF{%LA&bht_Dor#4AP0op1nkgnejBDu z>VdVnf=-#PEs#SVr(EM`C1d~!)VI#v19iBrGo5#40DCmY5KMln6017rj3)`^18{9? zpdSFoQH3}vyG`KM!^2~JV0i$l|F~frHPXrfVodzSrpx5uV>h_Myr(FD8I;5J9h<*j zPLADQh}c=>x|B&Gd4$}hwn)U!awUNBaeYR3R1Xi2^@o(eaXums0MHSSW9}6r2EZBF z|8*6}|8j+SJ1if++YRE}y--&mKc6R7p(LpoSa4FC*ZBZA&yA+idNaUpeuItLFp{^bjHNz_Grn!ER8Q;473+gV+MS` zJUMoEG2~~J>q^Fv;*p9$R_1{R1z`DcfW_m!Lx_jRx-Mrs_H0L=|59#)E>}WVHtE=#bZ9luBIk|3IG`Zc4EVDAfQ8#haiemx+Ia#wWTwm z-~8q`Slx#IpcbA0%a74iWJ>!j&uZz9N&lq&GnPp8uTS~N!ck2AaqGj@))st^9_0GK zYv%rGR*l?Q4B46GSju{m@|;kRE*=AcUdh53uyV<%5M1&29v&WDK>~;X#K;tR{1p;^ zQifCsRR2}LfS2pC7BTg1lK%U;BfhRQ0OBN<1AqMAK~*vK7dh$@0;*b1L7^wIG6SkQ zPg(#U-Qnrp!(%;cf7GzCA7@g7bp8m2`k)%4KTgTf9OIQ#edZ}2S@<#RTG{z$(S0HR zUhNBra`V^DRwZfyJWiyn#gg(4<@`}9No3K8wo#}%$%$5-{N}eUut}`z&1(jL9e)<5 z0n$26fvUzR{TZvosul|N=Oq2xbpKs510XHnc3@|I0BVZKzsL#Qz_n7>8A23~#FSv{ zHaoiAgadtcpx^ukYu%IrF|u7}+}6t=7mott9Llu>8-d8N9PmmiKl9WN&EMIJ^zVrQ zG;0CnObbX;kd=B$qp8z{Bf2X|SO7k{6N{31+P#N|hYP0D?F-|yQMKRvVPXiiL}KQj z5{$8Otm>iQJxl+dX9H5BklTTuxfg1;zES-aXYgfAMMhsh6mSqShIH;Qh>25ym8O7U z*)S$5V8epm&dbBY12E|)4}AP&8k>fyKl$1iN`GqpA*TvV9I426Kl7stcdA_=SQnu` z7pqF%rvvSU^p@9`_D&S|FV0j|8VV-H3L*Ic#MJ4)ij&$-Ir;zqHY@N^0S}L!A-0`C zS$_~$AUSeB#o!m~DO74w7BK9Ws{2JQde)PtrWbJVM>nBA7pv>$dA4pD22t>iTB7E$ z_#jS|LXRYw0<(|O31gvR1>N7Dm$m^fP0=fI?l2|XN#L+hO~@56k!QhdRqcJo)eE0h*J+J>l}B4W)NTma z9!PF`ed)8~dG?Cws!9XS#5fit(?v`HpEy-t)67AT3rBPV0}l_67O0H)QR<`WkFEFB z;xwc)kC0|PUrC@AOIeHTz)ZO^L@&DJ1M3W8EjKtAM@4`4%sXuuFtSeS0i7xeHLRxmwp zvqAikmi$QkXzVKnbCnn~fs<`T7ED*w-#zW>#n)K#d(DUM&s6|izZ-%}_ds&%>xv(m zDzaajt{_W;pcDnf1f)zsjGqd8WE(L7RBhzKF|utuJcc*XCm~0ZEyh`Y=~#}5N}_IFgz-zxKjqQtTI~MjGw2g23u!kcA|(6XQ24||p8t>OvWkI2 zSc(MX$aqOf6F?8A&1sgi2h317pC{lkWRMB4(smHzCu@fvsr{Px6Ukt~5I;uQYN@=n zz7Spfj8D%VmtA!F=@xx8#0?W*-+T0wdY)X*{ zV6qTJ2s{(;*vepfqBc6<#!l8gF(UqjX5z0B@ykMproTE~uJMbX@rlJ-Z4zD|&!)ey zeaLQk<@jW-7=O7CvI}Rct|ov!0Spmt2R?q1E*iq}#+d`4u)PtP1b*14$5sPtNc}KR z;c?3FGhMY%6UI*@5@4a0kmN4}AV8_(^D$g@-X|(I_nY{gvsxb}@by=YPwp(nUmne| z3y&F}0Qv)pk!AucH%i(7^*57}@&qVXfaVZ;ZNOuTNqr@O6(>MoVqAb}^C`%=Llhc_6vC5fXBds>9vc(4yFF+4q$}|-O(qmf}DATn(8thP@KOK0~f%tj=!``kI%y{{@Bc8{xCVd z8RLMxJQwRY(hDT#z$WvImFw!`hh*igFZejjuQ&0VGu{v|wHp{}1Gm1Wcv&RmHTjU8 zGKUF6$QRKhhy(~6J&w7>g>m2`J81k7og(%>ugp?!0n!L;*pRdWuNHW00&%a9@D)H1 zX~M^i2p`vPO$)NcW8~#m3{!h0RB@88f}|KifQdd5;D0^k>dIBP*KUS-^y}<54AznH z-B%S)8Oh6QN)fwk7AK7>NCgLx4>&~$uSSNh7T0uNLh4Fv zG^k8FXnF480qtnrA$YrHkl;lKEXdJd4kUPbE`ltjmu&pKSnf5$eE3}44xFjG^ z%d9O@3?DU4^jYEOusuF{_UkC|u~P<6q${w54-Ec6%Je;Fe>}d8_PGIG{*|z4D8aoM zx4gb|QV7YfGX-xdM*Jkqkiv>c)U+4_9&*O83vs{!I8&q<(52uHhYSF@HQHy$xsijD zxq$EE7kq}H#|mM*RGZE3WL2x}G~BgjGW>j3P(hk`u?HTQC?FkV88f|!9|EI>AkIu`o> z^!?A=|Hv^g(~3+0RPT^M_|Tz8Cn#h9^+u_#b^2wi3a4W;n#zM)EEQUwNcOnYf?@y= z2!Ki*4k-?wS2BHH6XBl?Y9qpX1IYk7F@n2(3Uci9=(0FruUEh>9f@F6Dr5#ags77T z5WsOzf44{<}wBAf)21=#RP855gQt*7G;LKbA4%RoO4GKhh=YL5+vB8`!SLTmmg)f<{5qn`Y=7|M*p102{*oT;yaR5vvd zyo|Z^9DmXkD)i$MI zd?W&nOkv7__u{$1fw0gkRO0CJ- zZcT1>3^W~3LecHOZa56=(i`aqY0GgR{B@X|E`thWbjnD9ojg+o2>f^g4+lvCFp>wD zucr%4C3io*IL^j*&dzosXjJcU-3=nqZy5&A3gpClcLCpVHZZsW0Dqi~%&Yd;dQhgC z6>v@64(iIYL0!3p*3_0_0G*I&{JZH#WP1T%*NHwUk4=TWW=+uJ-iAcqDunLg;o;%o l;o;%o;o;%o;Q>R9{~v7 + + + + + + + + + + true/pm + permonitorv2,permonitor + + + diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..fb4413e --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + 哈夫币发布计算器 + + +

+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..c200f0e --- /dev/null +++ b/frontend/package-lock.json @@ -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 + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..c167413 --- /dev/null +++ b/frontend/package.json @@ -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" + } +} diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 new file mode 100755 index 0000000..0db9035 --- /dev/null +++ b/frontend/package.json.md5 @@ -0,0 +1 @@ +93f3bcafbb7b2e5f01e2db048bd59072 \ No newline at end of file diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..9f603be --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,1111 @@ +import './style.css' + +// 版本号由 vite.config.js 从 wails.json 的 info.productVersion 注入。 +const appVersion = typeof __APP_VERSION__ === 'string' ? __APP_VERSION__ : 'dev' +const app = document.querySelector('#app') + +const state = { + backend: null, + config: null, + configPath: '', + serverURL: '', + form: { + insurance: '', + states: {}, + coinM: 0, + dailyLossM: 10, + saleRatio: 0, + quantities: {}, + freeItems: {}, + }, + calc: null, + configDraft: null, + configOpen: false, + activeTab: 'base', + syncMessage: '', +} + +app.innerHTML = ` +
+
+
+
哈
+
+

哈夫币发布计算器

+

按账号配置自动计算号主到手价

+
+
+
+ v${appVersion} + + +
+
+ +
+
+
+ +
+
+ +
+ +
+
+ +
+ +
+ + M +
+
+ +
+ +
+
+ +
+ +
+ 1 : + +
+

填写资料后自动计算参考比例

+
+ +
+ 计算口径 +

参考比例 = 保险基数 + 缺配惩罚 + 大额修正 + 每日损耗。比例越大,号主到手越低。

+
+
+ +
+
+
+ +

数量按单发/单个填写;免费物品不计入到手价

+
+ 0 项 +
+
+
+
+ +
+
+ 哈弗币到手 + ¥0.00 +
+
+ 比例 + -- +
+
+ 物品到手 + ¥0.00 +
+
+ 合计到手 + ¥0.00 +
+
+
+ + + +
+ +` + +const els = { + insuranceGrid: document.querySelector('#insuranceGrid'), + stateGrid: document.querySelector('#stateGrid'), + lossRow: document.querySelector('#lossRow'), + itemList: document.querySelector('#itemList'), + itemsCount: document.querySelector('#itemsCount'), + coinInput: document.querySelector('#coinInput'), + ratioInput: document.querySelector('#ratioInput'), + ratioHint: document.querySelector('#ratioHint'), + coinResult: document.querySelector('#coinResult'), + ratioResult: document.querySelector('#ratioResult'), + itemResult: document.querySelector('#itemResult'), + totalResult: document.querySelector('#totalResult'), + configModalRoot: document.querySelector('#configModalRoot'), + toast: document.querySelector('#toast'), + bootError: document.querySelector('#bootError'), + bootErrorText: document.querySelector('#bootErrorText'), +} + +let toastTimer = 0 +let recalcTimer = 0 +let calcSequence = 0 +let lastRatioValue = null + +boot() + +async function boot() { + try { + state.backend = await waitForBackend() + const bootstrap = await call('Bootstrap') + state.config = bootstrap.config + state.configPath = bootstrap.config_path || '' + state.serverURL = bootstrap.server_url || state.config?.sync_url || '' + initFormState(true) + renderMain() + await recalc() + window.requestAnimationFrame(() => els.coinInput.focus()) + if (bootstrap.load_error) { + toast(bootstrap.load_error, 'error', 6000) + } + } catch (error) { + els.bootError.hidden = false + els.bootErrorText.textContent = errorText(error) + } +} + +async function waitForBackend() { + for (let index = 0; index < 120; index += 1) { + const backend = window.go?.main?.App + if (backend) return backend + await new Promise(resolve => setTimeout(resolve, 50)) + } + throw new Error('桌面后端未就绪,请通过 Wails 客户端启动本程序。') +} + +async function call(method, ...args) { + const backend = state.backend || window.go?.main?.App + if (!backend || typeof backend[method] !== 'function') { + throw new Error(`后端方法 ${method} 不可用`) + } + return backend[method](...args) +} + +function initFormState(reset) { + const config = state.config + if (!config) return + + if (reset) { + state.form = { + insurance: config.insurance_base_ratios?.[0]?.insurance || '', + states: {}, + coinM: 0, + dailyLossM: config.daily_loss_options?.[0] ?? 10, + saleRatio: 0, + quantities: {}, + freeItems: {}, + } + } + + const insuranceKeys = new Set((config.insurance_base_ratios || []).map(item => item.insurance)) + if (!insuranceKeys.has(state.form.insurance)) { + state.form.insurance = config.insurance_base_ratios?.[0]?.insurance || '' + } + + const stateKeys = new Set((config.ratio_config_items || []).map(item => item.key)) + for (const key of Object.keys(state.form.states)) { + if (!stateKeys.has(key)) delete state.form.states[key] + } + for (const key of stateKeys) { + if (state.form.states[key] === undefined) state.form.states[key] = false + } + + const quantityKeys = new Set((config.quantity_items || []).map(item => item.key)) + for (const key of Object.keys(state.form.quantities)) { + if (!quantityKeys.has(key)) delete state.form.quantities[key] + } + for (const key of Object.keys(state.form.freeItems)) { + if (!quantityKeys.has(key)) delete state.form.freeItems[key] + } + for (const key of quantityKeys) { + if (state.form.quantities[key] === undefined) state.form.quantities[key] = 0 + if (state.form.freeItems[key] === undefined) state.form.freeItems[key] = false + } + + const lossOptions = config.daily_loss_options || [] + if (!lossOptions.includes(Number(state.form.dailyLossM))) { + state.form.dailyLossM = lossOptions[0] ?? 10 + } +} + +function renderMain() { + if (!state.config) return + renderInsurance() + renderStates() + renderLoss() + renderItems() + els.coinInput.value = state.form.coinM > 0 ? String(state.form.coinM) : '' + els.ratioInput.value = state.form.saleRatio > 0 ? String(state.form.saleRatio) : '' + renderResults(state.calc) +} + +function renderInsurance() { + const items = state.config?.insurance_base_ratios || [] + els.insuranceGrid.innerHTML = items + .map( + item => ` + + ` + ) + .join('') +} + +function renderStates() { + const order = ['melee', 'operatorRed', 'staminaMax', 'loadMax', 'weapon'] + const items = [...(state.config?.ratio_config_items || [])].sort((left, right) => { + const leftIndex = order.indexOf(left.key) + const rightIndex = order.indexOf(right.key) + if (leftIndex >= 0 && rightIndex >= 0) return leftIndex - rightIndex + if (leftIndex >= 0) return -1 + if (rightIndex >= 0) return 1 + return 0 + }) + const selectedCount = items.filter(item => state.form.states[item.key]).length + const countEl = document.querySelector('#stateCount') + if (countEl) countEl.textContent = `已选 ${selectedCount} 项` + els.stateGrid.innerHTML = items + .map((item, index) => { + const wide = items.length % 2 === 1 && index === items.length - 1 ? ' wide' : '' + return ` + + ` + }) + .join('') +} + +function renderLoss() { + const options = state.config?.daily_loss_options || [] + els.lossRow.innerHTML = options + .map( + value => ` + + ` + ) + .join('') +} + +function renderItems() { + const items = state.config?.quantity_items || [] + let filled = 0 + + els.itemList.innerHTML = items + .map(item => { + const disabled = isGridCardDisabled(item) + if (disabled && Number(state.form.quantities[item.key] || 0) !== 0) { + state.form.quantities[item.key] = 0 + } + const quantity = disabled ? 0 : Number(state.form.quantities[item.key] || 0) + if (quantity > 0 && !state.form.freeItems[item.key]) filled += 1 + const free = Boolean(state.form.freeItems[item.key]) + return ` +
+
+ ${escapeHTML(item.label)} + ${escapeHTML(item.price)}${disabled ? ' · 9格保险下不可用' : ''} +
+ +
+ +
+ + ${escapeHTML(item.unit || '个')} +
+ +
+
+ ` + }) + .join('') + + els.itemsCount.textContent = `${filled} 项` +} + +function renderResults(result) { + if (!result) { + els.coinResult.textContent = '¥0.00' + els.ratioResult.textContent = '--' + els.itemResult.textContent = '¥0.00' + els.totalResult.textContent = '¥0.00' + return + } + + els.coinResult.textContent = money(result.coin_base_price) + const normalizedRatio = Number(result.final_ratio || 0) + const ratioChanged = lastRatioValue !== null && normalizedRatio !== lastRatioValue + lastRatioValue = normalizedRatio + els.ratioResult.textContent = result.final_ratio > 0 ? formatNumber(result.final_ratio) : '--' + els.itemResult.textContent = money(result.item_price) + els.totalResult.textContent = money(result.total_price) + if (ratioChanged) { + flashElement(els.ratioResult) + flashElement(els.ratioHint) + } + + const reference = Number(result.reference_ratio || 0) + const maxRatio = Number(result.max_ratio || 0) + els.ratioInput.disabled = reference <= 0 + if (reference > 0) { + els.ratioInput.placeholder = `留空使用 ${formatNumber(reference)}` + } else { + els.ratioInput.placeholder = '填写资料后自动生成' + } + + const parts = [] + if (result.insurance_base_ratio > 0) parts.push(`基础 ${formatNumber(result.insurance_base_ratio)}`) + if (result.config_penalty > 0) parts.push(`缺配 +${formatNumber(result.config_penalty)}`) + if (result.coin_correction > 0) parts.push(`大额 +${formatNumber(result.coin_correction)}`) + if (result.daily_loss_adjustment > 0) parts.push(`损耗 +${formatNumber(result.daily_loss_adjustment)}`) + if (result.final_ratio > reference) { + parts.push(`加速 +${formatNumber(result.final_ratio - reference)}`) + } + + if (reference > 0) { + const summary = [`参考 1:${formatNumber(reference)}`, `可调至 1:${formatNumber(maxRatio)}`] + if (result.final_ratio > reference) { + summary.push(`当前 1:${formatNumber(result.final_ratio)}`) + } + els.ratioHint.textContent = [...summary, ...parts].join(' · ') + els.ratioHint.dataset.tone = 'ok' + } else { + els.ratioHint.textContent = result.message || '填写资料后自动计算参考比例' + els.ratioHint.dataset.tone = result.valid ? 'neutral' : 'error' + } +} + +function scheduleRecalc() { + window.clearTimeout(recalcTimer) + recalcTimer = window.setTimeout(recalc, 80) +} + +async function recalc() { + if (!state.config) return + const sequence = ++calcSequence + const input = { + coin_m: numberValue(state.form.coinM), + insurance: state.form.insurance, + states: state.form.states, + daily_loss_m: numberValue(state.form.dailyLossM), + sale_ratio: numberValue(state.form.saleRatio), + quantities: state.form.quantities, + free_items: state.form.freeItems, + } + try { + const result = await call('Calculate', input) + if (sequence !== calcSequence) return + state.calc = result + renderResults(result) + } catch (error) { + if (sequence !== calcSequence) return + toast(errorText(error), 'error') + } +} + +function handleClick(event) { + const backdrop = event.target.closest('#configBackdrop') + if (backdrop && event.target === backdrop) { + closeConfig() + return + } + + const trigger = event.target.closest('[data-action]') + if (!trigger) return + const action = trigger.dataset.action + + switch (action) { + case 'select-insurance': { + state.form.insurance = trigger.dataset.value || '' + if (state.form.insurance === '3*3') { + for (const item of state.config.quantity_items || []) { + if (isGridCardDisabled(item)) state.form.quantities[item.key] = 0 + } + } + renderInsurance() + renderItems() + scheduleRecalc() + break + } + case 'toggle-state': { + const key = trigger.dataset.key || '' + state.form.states[key] = !state.form.states[key] + renderStates() + scheduleRecalc() + break + } + case 'select-loss': { + state.form.dailyLossM = numberValue(trigger.dataset.value) + renderLoss() + scheduleRecalc() + break + } + case 'clear-ratio': { + state.form.saleRatio = 0 + els.ratioInput.value = '' + els.ratioInput.focus() + scheduleRecalc() + break + } + case 'focus-coin': { + els.coinInput.focus() + break + } + case 'focus-ratio': { + if (!els.ratioInput.disabled) els.ratioInput.focus() + break + } + case 'item-minus': + case 'item-plus': { + const key = trigger.dataset.key || '' + const direction = action === 'item-plus' ? 1 : -1 + const step = event.shiftKey ? 10 : 1 + const next = Math.max(0, numberValue(state.form.quantities[key]) + direction * step) + state.form.quantities[key] = next + const input = findQuantityInput(key) + if (input) input.value = String(next) + scheduleRecalc() + break + } + case 'toggle-free': { + const key = trigger.dataset.key || '' + state.form.freeItems[key] = !state.form.freeItems[key] + renderItems() + scheduleRecalc() + break + } + case 'open-config': + openConfig('base') + break + case 'sync-top': + openConfig('sync') + break + case 'close-config': + closeConfig() + break + case 'config-tab': + state.activeTab = trigger.dataset.tab || 'base' + renderConfigModal() + break + case 'add-row': + addConfigRow(trigger.dataset.section || '') + break + case 'remove-row': + removeConfigRow(trigger.dataset.section || '', Number(trigger.dataset.index || 0)) + break + case 'save-config': + saveConfig() + break + case 'reset-config': + resetConfig() + break + case 'import-config': + importConfig() + break + case 'export-config': + exportConfig() + break + case 'sync-online': + syncOnline() + break + case 'open-folder': + openConfigFolder() + break + default: + break + } +} + +function handleInput(event) { + const input = event.target.closest('[data-input]') + if (input) { + const kind = input.dataset.input + if (kind === 'coin') { + state.form.coinM = numberValue(input.value) + scheduleRecalc() + } else if (kind === 'ratio') { + state.form.saleRatio = numberValue(input.value) + scheduleRecalc() + } else if (kind === 'qty') { + const key = input.dataset.key || '' + state.form.quantities[key] = Math.max(0, numberValue(input.value)) + scheduleRecalc() + } + return + } + + const configInput = event.target.closest('[data-config]') + if (configInput) updateConfigDraft(configInput) +} + +function handleChange(event) { + const configInput = event.target.closest('[data-config]') + if (configInput) updateConfigDraft(configInput) +} + +function updateConfigDraft(input) { + const draft = state.configDraft + if (!draft) return + const section = input.dataset.config + const index = Number(input.dataset.index || 0) + const field = input.dataset.field || '' + let value = input.type === 'checkbox' ? input.checked : input.value + if (input.type === 'number') value = numberValue(value) + + if (section === 'insurance' && draft.insurance_base_ratios[index]) { + draft.insurance_base_ratios[index][field] = value + } else if (section === 'ratio-item' && draft.ratio_config_items[index]) { + draft.ratio_config_items[index][field] = value + } else if (section === 'correction' && draft.coin_corrections[index]) { + draft.coin_corrections[index][field] = value + } else if (section === 'quantity' && draft.quantity_items[index]) { + draft.quantity_items[index][field] = value + } else if (section === 'scalar') { + if (field === 'accelerated_max_ratio') { + draft.accelerated_max_ratio = numberValue(value) + } else if (field === 'daily_loss_options') { + draft.daily_loss_options = String(value) + .split(/[,,\s]+/) + .map(item => Number(item)) + .filter(item => Number.isFinite(item) && item > 0) + } else if (field === 'sync_url') { + draft.sync_url = String(value) + } + } +} + +function openConfig(tab) { + if (!state.config) return + state.configDraft = deepClone(state.config) + state.configOpen = true + state.activeTab = tab || 'base' + state.syncMessage = '' + renderConfigModal() +} + +function closeConfig() { + state.configOpen = false + state.configDraft = null + state.syncMessage = '' + renderConfigModal() +} + +function renderConfigModal() { + if (!state.configOpen || !state.configDraft) { + els.configModalRoot.innerHTML = '' + return + } + + const tabs = [ + { key: 'base', label: '基础比例' }, + { key: 'states', label: '缺配惩罚' }, + { key: 'corrections', label: '大额修正' }, + { key: 'items', label: '物品单价' }, + { key: 'sync', label: '线上同步' }, + ] + + els.configModalRoot.innerHTML = ` + + ` + renderConfigBody() +} + +function renderConfigBody() { + const body = document.querySelector('#configBody') + const draft = state.configDraft + if (!body || !draft) return + + if (state.activeTab === 'base') { + body.innerHTML = ` +
+
+

保险格数基础比例

+

对应主站发布规则:3*3=9格、2*3=6格、2*2=4格、2*1=2格。

+
+ +
+
+ + + + ${draft.insurance_base_ratios + .map( + (item, index) => ` + + + + + + ` + ) + .join('')} + +
保险格数基础比例
+
+ ` + return + } + + if (state.activeTab === 'states') { + body.innerHTML = ` +
+
+

缺配惩罚

+

每缺少一项,比例增加对应点数;全部满足时不加。

+
+ +
+
+ + + + ${draft.ratio_config_items + .map( + (item, index) => ` + + + + + + + + ` + ) + .join('')} + +
key显示名称类型缺少惩罚
+ +
+
+ ` + return + } + + if (state.activeTab === 'corrections') { + body.innerHTML = ` +
+
+

大额哈夫币修正

+

严格大于阈值才命中,并取命中的最高档;不会累加。

+
+ +
+
+ + + + ${draft.coin_corrections + .map( + (item, index) => ` + + + + + + ` + ) + .join('')} + +
大于(M)比例增加
+
+ ` + return + } + + if (state.activeTab === 'items') { + body.innerHTML = ` +
+
+

物品单价

+

单价支持「0.6元/发」「6元/60发」「2.5元/个」;停用后不会出现在计算器里。

+
+ +
+
+ + + + ${draft.quantity_items + .map( + (item, index) => ` + + + + + + + + + ` + ) + .join('')} + +
key名称单价文本单位停用
+
+ ` + return + } + + if (state.activeTab === 'sync') { + body.innerHTML = ` +
+
+
+

从线上拉取规则

+

调用公开接口 GET /api/listing-publish-options,只覆盖保险、缺配、大额修正和物品。

+
+
+ +
+ ${escapeHTML(state.syncMessage || '同步后会自动保存到本地;每日损耗、加速上限和本地停用项会保留。')} +
+
+

默认地址:https://hao858.com

+

如果线上规则还没发布,可以继续使用本地配置,或在其他页签手工维护。

+
+
+
+

其他参数

+
+ + +
+
+ ` + return + } +} + +function addConfigRow(section) { + const draft = state.configDraft + if (!draft) return + if (section === 'insurance') { + draft.insurance_base_ratios.push({ insurance: '', ratio: 0 }) + } else if (section === 'ratio') { + draft.ratio_config_items.push({ + key: '', + label: '', + kind: 'skin_group', + group_key: '', + missing_penalty: 1, + }) + } else if (section === 'corrections') { + draft.coin_corrections.push({ threshold_m: 0, correction: 0 }) + } else if (section === 'quantity') { + draft.quantity_items.push({ + key: '', + label: '', + price: '', + unit: '个', + placeholder: '', + disabled: false, + }) + } + renderConfigBody() +} + +function removeConfigRow(section, index) { + const draft = state.configDraft + if (!draft) return + if (section === 'insurance') draft.insurance_base_ratios.splice(index, 1) + if (section === 'ratio') draft.ratio_config_items.splice(index, 1) + if (section === 'corrections') draft.coin_corrections.splice(index, 1) + if (section === 'quantity') draft.quantity_items.splice(index, 1) + renderConfigBody() +} + +async function saveConfig() { + if (!state.configDraft) return + try { + const saved = await call('SaveConfig', state.configDraft) + state.config = saved + state.serverURL = saved.sync_url || state.serverURL + initFormState(false) + closeConfig() + renderMain() + await recalc() + toast('规则已保存并应用') + } catch (error) { + toast(errorText(error), 'error', 6000) + } +} + +async function resetConfig() { + if (!window.confirm('恢复内置默认规则?当前本地配置会被覆盖。')) return + try { + const config = await call('ResetConfig') + state.config = config + state.serverURL = config.sync_url || state.serverURL + initFormState(false) + closeConfig() + renderMain() + await recalc() + toast('已恢复默认规则') + } catch (error) { + toast(errorText(error), 'error', 6000) + } +} + +async function importConfig() { + try { + const result = await call('ImportConfig') + if (result.cancelled) return + state.config = result.config + state.serverURL = result.config.sync_url || state.serverURL + initFormState(false) + closeConfig() + renderMain() + await recalc() + toast(`已导入:${result.path}`, 'ok', 5000) + } catch (error) { + toast(errorText(error), 'error', 6000) + } +} + +async function exportConfig() { + try { + const result = await call('ExportConfig') + if (result.cancelled) return + toast(`已导出:${result.path}`, 'ok', 5000) + } catch (error) { + toast(errorText(error), 'error', 6000) + } +} + +async function syncOnline() { + const draft = state.configDraft + if (!draft) return + const url = String(draft.sync_url || state.serverURL || '').trim() + state.syncMessage = '正在拉取线上规则...' + renderConfigBody() + try { + const result = await call('SyncFromServer', url) + state.config = result.config + state.configDraft = deepClone(result.config) + state.serverURL = result.server_url + state.syncMessage = `已同步 ${result.insurance_count} 档保险、${result.ratio_item_count} 项缺配、${result.coin_correction_count} 档大额修正、${result.quantity_item_count} 个物品(${result.synced_at})` + initFormState(false) + renderMain() + await recalc() + renderConfigModal() + toast('线上规则已同步并保存') + } catch (error) { + state.syncMessage = `同步失败:${errorText(error)}` + renderConfigBody() + toast('线上同步失败', 'error', 5000) + } +} + +async function openConfigFolder() { + try { + await call('OpenConfigFolder') + } catch (error) { + toast(errorText(error), 'error', 5000) + } +} + +function findQuantityInput(key) { + return [...document.querySelectorAll('input[data-input="qty"]')].find( + input => input.dataset.key === key + ) +} + +function isGridCardDisabled(item) { + if (item.disabled) return true + if (state.form.insurance !== '3*3') return false + return ( + item.key === 'gridCard9' || + String(item.label || '').includes('9格') || + String(item.label || '').includes('顶级保险卡') + ) +} + +function insuranceLabel(insurance) { + const labels = { + '3*3': '9格', + '2*3': '6格', + '2*2': '4格', + '2*1': '2格', + } + return labels[insurance] || insurance +} + +function stateLabel(item) { + const labels = { + melee: '有刀皮', + operatorRed: '有角色红皮', + staminaMax: '满体力', + loadMax: '满负重', + weapon: '有武器皮肤', + } + if (labels[item.key]) return labels[item.key] + if (item.kind === 'max_stamina' || item.kind === 'max_load') return item.label + return `有${item.label}` +} + +function numberValue(value) { + const number = Number(value) + return Number.isFinite(number) && number > 0 ? number : 0 +} + +function formatNumber(value) { + const number = Math.round((Number(value) || 0) * 10) / 10 + return Number.isInteger(number) ? String(number) : number.toFixed(1) +} + +function money(value) { + return `¥${(Number(value) || 0).toFixed(2)}` +} + +function deepClone(value) { + return JSON.parse(JSON.stringify(value)) +} + +function errorText(error) { + if (!error) return '未知错误' + if (typeof error === 'string') return error + return error.message || String(error) +} + +function escapeHTML(value) { + return String(value ?? '').replace(/[&<>"']/g, char => { + const map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + } + return map[char] + }) +} + +function toast(message, tone = 'ok', duration = 3000) { + els.toast.textContent = message + els.toast.dataset.tone = tone + els.toast.hidden = false + window.clearTimeout(toastTimer) + toastTimer = window.setTimeout(() => { + els.toast.hidden = true + }, duration) +} + +function flashElement(element) { + if (!element) return + element.classList.remove('is-flash') + void element.offsetWidth + element.classList.add('is-flash') + window.setTimeout(() => element.classList.remove('is-flash'), 520) +} + +app.addEventListener('click', handleClick) +app.addEventListener('input', handleInput) +app.addEventListener('change', handleChange) diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..435a36d --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,1202 @@ +:root { + color: #1f2937; + background: #eef2f7; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; + font-size: 14px; + --bg: #eef2f7; + --panel: #ffffff; + --ink: #1f2937; + --muted: #7c8798; + --line: #e3e8f0; + --line-strong: #cfd8e6; + --accent: #f08a24; + --accent-strong: #d9720c; + --accent-soft: #fff3e5; + --blue: #3568d4; + --blue-soft: #edf3ff; + --danger: #c24141; +} + +* { + box-sizing: border-box; +} + +[hidden] { + display: none !important; +} + +body { + margin: 0; + overflow: hidden; + min-height: 100vh; + background: + radial-gradient(circle at 12% 0%, rgba(240, 138, 36, 0.08), transparent 30%), + radial-gradient(circle at 88% 0%, rgba(53, 104, 212, 0.08), transparent 32%), + var(--bg); +} + +button, +input, +select { + font: inherit; +} + +button { + cursor: pointer; +} + +#app, +.app-shell { + height: 100vh; +} + +.app-shell { + display: flex; + flex-direction: column; +} + +.topbar { + height: 76px; + flex-shrink: 0; + padding: 0 24px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; +} + +.brand { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} + +.brand-mark { + width: 44px; + height: 44px; + flex-shrink: 0; + border-radius: 13px; + display: grid; + place-items: center; + background: linear-gradient(145deg, #ffb35c, #ec7a10); + color: #fff; + font-size: 21px; + font-weight: 800; + box-shadow: 0 8px 18px rgba(236, 122, 16, 0.24); +} + +.brand-text h1 { + margin: 0; + font-size: 21px; + line-height: 1.25; + letter-spacing: 0.2px; +} + +.brand-text p { + margin: 3px 0 0; + color: var(--muted); + font-size: 12px; +} + +.topbar-actions { + display: flex; + align-items: center; + gap: 9px; + flex-shrink: 0; +} + +.version { + padding: 7px 9px; + border: 1px solid #d8e3fb; + border-radius: 8px; + background: #f4f7ff; + color: #3f64b8; + font-size: 12px; + font-weight: 700; + white-space: nowrap; +} + +.button { + height: 38px; + padding: 0 15px; + border: 1px solid var(--line-strong); + border-radius: 9px; + background: #fff; + color: var(--ink); + font-weight: 650; + transition: + transform 0.12s ease, + box-shadow 0.12s ease, + background 0.12s ease; +} + +.button:hover { + box-shadow: 0 5px 14px rgba(31, 41, 55, 0.09); +} + +.button:active { + transform: translateY(1px); +} + +.button.ghost { + background: #fff; + color: #4a5568; +} + +.button.dark { + border-color: #283447; + background: #283447; + color: #fff; +} + +.button.primary { + border-color: var(--accent); + background: var(--accent); + color: #fff; +} + +.button.primary:hover { + background: var(--accent-strong); +} + +.button:disabled, +.icon-button:disabled { + cursor: not-allowed; + opacity: 0.55; +} + +.layout { + flex: 1; + min-height: 0; + padding: 0 22px 14px; + display: grid; + grid-template-columns: minmax(390px, 46%) minmax(0, 1fr); + gap: 14px; +} + +.panel { + min-height: 0; + border: 1px solid var(--line); + border-radius: 15px; + background: var(--panel); + box-shadow: 0 10px 28px rgba(31, 41, 55, 0.045); +} + +.left-panel { + padding: 18px; + overflow: auto; + display: flex; + flex-direction: column; + gap: 15px; +} + +.right-panel { + padding: 18px 20px 8px; + display: flex; + flex-direction: column; +} + +.panel-section { + min-width: 0; +} + +.section-label { + margin-bottom: 8px; + color: #667085; + font-size: 12px; + font-weight: 750; + letter-spacing: 0.3px; +} + +.section-count { + padding: 2px 7px; + border-radius: 999px; + background: #f0f4fa; + color: #8d98a8; + font-size: 10.5px; + font-weight: 650; + letter-spacing: 0; +} + +.row-label { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.insurance-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 8px; +} + +.choice { + min-height: 58px; + padding: 8px 6px; + border: 1px solid var(--line); + border-radius: 10px; + background: #f9fbfd; + color: #667085; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 3px; + transition: + border-color 0.12s ease, + background 0.12s ease, + color 0.12s ease, + transform 0.12s ease; +} + +.choice:hover { + border-color: #b9c8e6; + background: #f5f8ff; +} + +.choice strong { + font-size: 15px; + font-weight: 750; +} + +.choice small { + color: #9aa5b5; + font-size: 11px; + font-weight: 600; +} + +.choice.is-active { + border-color: #7da0e8; + background: var(--blue-soft); + color: var(--blue); + box-shadow: inset 0 0 0 1px rgba(53, 104, 212, 0.16); +} + +.choice.is-active small { + color: #5f7fc6; +} + +.state-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.state-chip { + min-height: 39px; + padding: 0 12px; + border: 1px solid var(--line); + border-radius: 9px; + background: #f9fbfd; + color: #5f6b7a; + font-weight: 650; + display: flex; + align-items: center; + justify-content: center; + gap: 5px; + transition: + border-color 0.12s ease, + background 0.12s ease, + color 0.12s ease, + box-shadow 0.12s ease; +} + +.state-chip:hover { + border-color: #b9c8e6; + background: #f5f8ff; +} + +.state-chip.wide { + grid-column: 1 / -1; +} + +.state-chip.is-active { + border-color: #527fd7; + background: var(--blue-soft); + color: var(--blue); + box-shadow: + inset 0 0 0 1px rgba(53, 104, 212, 0.16), + 0 3px 10px rgba(53, 104, 212, 0.08); +} + +.state-chip.is-active::after { + content: "✓"; + font-size: 11px; + font-weight: 800; +} + +.coin-input { + height: 52px; + padding: 0 14px; + border: 1px solid var(--line-strong); + border-radius: 11px; + background: #fff; + display: flex; + align-items: center; + gap: 8px; + cursor: text; + transition: + border-color 0.12s ease, + box-shadow 0.12s ease, + background 0.12s ease; +} + +.coin-input:hover { + border-color: #b9c8e6; +} + +.coin-input:focus-within { + border-color: #3568d4; + background: #f8fbff; + box-shadow: + 0 0 0 4px rgba(53, 104, 212, 0.16), + 0 5px 16px rgba(53, 104, 212, 0.08); +} + +.coin-input input { + width: 100%; + height: 100%; + border: none; + outline: none; + background: transparent; + color: var(--ink); + font-size: 26px; + font-weight: 760; + letter-spacing: 0.4px; + caret-color: var(--accent); +} + +.coin-input:focus-within span { + color: var(--blue); +} + +.coin-input span { + color: #8d98a8; + font-size: 14px; + font-weight: 750; +} + +.loss-row { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.mini-chip { + min-height: 30px; + padding: 0 10px; + border: 1px solid var(--line); + border-radius: 8px; + background: #f9fbfd; + color: #7c8798; + font-size: 12px; + font-weight: 650; +} + +.mini-chip:hover { + border-color: #b9c8e6; +} + +.mini-chip.is-active { + border-color: #7da0e8; + background: var(--blue-soft); + color: var(--blue); +} + +.ratio-section { + margin-top: 0; +} + +.ratio-input-row { + height: 43px; + border: 1px solid var(--line-strong); + border-radius: 10px; + background: #fff; + display: flex; + align-items: center; + gap: 8px; + padding: 0 12px; + cursor: text; + transition: + border-color 0.12s ease, + box-shadow 0.12s ease, + background 0.12s ease; +} + +.ratio-input-row:hover { + border-color: #b9c8e6; +} + +.ratio-input-row:focus-within { + border-color: #3568d4; + background: #f8fbff; + box-shadow: 0 0 0 4px rgba(53, 104, 212, 0.14); +} + +.ratio-prefix { + flex-shrink: 0; + color: #8d98a8; + font-size: 13px; + font-weight: 750; + white-space: nowrap; +} + +.ratio-input-row input { + width: 100%; + height: 100%; + border: none; + outline: none; + background: transparent; + color: var(--ink); + font-size: 16px; + font-weight: 700; + caret-color: var(--accent); + -moz-appearance: textfield; + appearance: textfield; +} + +.ratio-input-row input::-webkit-outer-spin-button, +.ratio-input-row input::-webkit-inner-spin-button { + margin: 0; + -webkit-appearance: none; +} + +.calc-note { + margin-top: auto; + padding: 12px 13px; + border: 1px dashed #d9e2ef; + border-radius: 10px; + background: #fafcff; +} + +.calc-note strong { + color: #667085; + font-size: 11.5px; + font-weight: 750; +} + +.calc-note p { + margin: 4px 0 0; + color: #8d98a8; + font-size: 11.5px; + line-height: 1.55; +} + +.hint { + margin: 6px 0 0; + color: #8d98a8; + font-size: 11.5px; + line-height: 1.5; +} + +.hint[data-tone="ok"] { + color: #758399; +} + +.hint[data-tone="error"] { + color: var(--danger); +} + +.text-button { + padding: 0; + border: none; + background: transparent; + color: #6d83ad; + font-size: 11.5px; + font-weight: 650; +} + +.text-button:hover { + color: var(--blue); + text-decoration: underline; +} + +.items-head { + flex-shrink: 0; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + padding-bottom: 8px; + border-bottom: 1px solid var(--line); +} + +.items-head .hint { + margin-top: 2px; +} + +.items-count { + margin-top: 2px; + padding: 5px 8px; + border-radius: 7px; + background: #f4f7fb; + color: #7c8798; + font-size: 11px; + font-weight: 700; + white-space: nowrap; +} + +.item-list { + min-height: 0; + flex: 1; + overflow: auto; + padding-right: 3px; +} + +.item-row { + display: grid; + grid-template-columns: minmax(0, 1fr) 50px 142px; + align-items: center; + gap: 10px; + padding: 10px 2px; + border-bottom: 1px dashed #e8edf4; +} + +.item-row:last-child { + border-bottom: none; +} + +.item-main { + min-width: 0; +} + +.item-main strong { + display: block; + color: #2f3b4c; + font-size: 13.5px; + font-weight: 700; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.item-main span { + display: block; + margin-top: 2px; + color: #9aa5b5; + font-size: 11.5px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.free-toggle { + height: 29px; + padding: 0 6px; + border: 1px solid var(--line); + border-radius: 8px; + background: #f9fbfd; + color: #7c8798; + font-size: 11px; + font-weight: 700; +} + +.free-toggle:hover { + border-color: #f0c188; +} + +.free-toggle.is-free { + border-color: #f3c48d; + background: var(--accent-soft); + color: #b9650a; +} + +.stepper { + display: grid; + grid-template-columns: 29px minmax(0, 1fr) 29px; + align-items: center; + gap: 5px; +} + +.stepper > button { + width: 29px; + height: 29px; + padding: 0; + border: 1px solid #dce4ef; + border-radius: 7px; + background: #f6f9fd; + color: #5f6b7a; + font-size: 17px; + line-height: 1; + font-weight: 500; +} + +.stepper > button:hover { + border-color: #b9c8e6; + background: #edf3ff; + color: var(--blue); +} + +.qty-box { + height: 31px; + border: 1px solid #dce4ef; + border-radius: 7px; + background: #fff; + display: flex; + align-items: center; + gap: 3px; + padding: 0 6px; +} + +.qty-box:focus-within { + border-color: #3568d4; + background: #f8fbff; + box-shadow: 0 0 0 3px rgba(53, 104, 212, 0.13); +} + +.qty-box input { + width: 100%; + min-width: 0; + height: 100%; + border: none; + outline: none; + background: transparent; + color: #344054; + text-align: center; + font-size: 12.5px; + font-weight: 700; + -moz-appearance: textfield; + appearance: textfield; +} + +.qty-box input::-webkit-outer-spin-button, +.qty-box input::-webkit-inner-spin-button, +.coin-input input::-webkit-outer-spin-button, +.coin-input input::-webkit-inner-spin-button { + margin: 0; + -webkit-appearance: none; +} + +.qty-box span { + flex-shrink: 0; + color: #9aa5b5; + font-size: 10.5px; + font-weight: 650; +} + +.item-row.is-disabled { + opacity: 0.46; +} + +.item-row.is-disabled .stepper > button, +.item-row.is-disabled .free-toggle { + cursor: not-allowed; +} + +.result-bar { + flex-shrink: 0; + margin: 0 22px 18px; + min-height: 92px; + border: 1px solid var(--line); + border-radius: 15px; + background: #fff; + box-shadow: 0 10px 28px rgba(31, 41, 55, 0.05); + display: grid; + grid-template-columns: 1.25fr 0.75fr 1.05fr 1fr; + align-items: center; +} + +.result-cell { + min-width: 0; + padding: 0 20px; + display: flex; + flex-direction: column; + gap: 4px; +} + +.result-cell + .result-cell { + border-left: 1px solid var(--line); +} + +.result-cell span { + color: #8d98a8; + font-size: 11.5px; + font-weight: 700; +} + +.result-money { + color: var(--accent); + font-size: 25px; + line-height: 1.1; + font-weight: 800; + letter-spacing: 0.2px; + white-space: nowrap; +} + +.result-ratio { + color: #243043; + font-size: 25px; + line-height: 1.1; + font-weight: 800; + white-space: nowrap; +} + +.result-cell.total .result-money { + color: #e46f12; +} + +@keyframes valueFlash { + 0% { + background: #fff0dc; + box-shadow: 0 0 0 4px rgba(240, 138, 36, 0.16); + } + 100% { + background: transparent; + box-shadow: 0 0 0 0 rgba(240, 138, 36, 0); + } +} + +.is-flash { + border-radius: 7px; + animation: valueFlash 0.52s ease; +} + +.boot-error { + position: fixed; + inset: 0; + z-index: 80; + display: grid; + place-items: center; + background: rgba(245, 247, 251, 0.97); +} + +.boot-error-card { + width: min(460px, calc(100vw - 40px)); + padding: 24px; + border: 1px solid var(--line); + border-radius: 15px; + background: #fff; + box-shadow: 0 18px 48px rgba(31, 41, 55, 0.12); +} + +.boot-error-card h2 { + margin: 0 0 8px; + font-size: 19px; +} + +.boot-error-card p { + margin: 0; + color: var(--muted); + line-height: 1.6; +} + +.modal-backdrop { + position: fixed; + inset: 0; + z-index: 40; + padding: 20px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(15, 23, 42, 0.46); + backdrop-filter: blur(2px); +} + +.modal-card { + width: min(960px, 100%); + max-height: calc(100vh - 40px); + border-radius: 16px; + background: #fff; + box-shadow: 0 24px 70px rgba(15, 23, 42, 0.28); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.modal-head { + padding: 18px 22px 14px; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 14px; + border-bottom: 1px solid var(--line); +} + +.modal-head h2 { + margin: 0; + font-size: 18px; + line-height: 1.3; +} + +.modal-head p { + margin: 4px 0 0; + color: var(--muted); + font-size: 12px; +} + +.icon-button { + width: 32px; + height: 32px; + flex-shrink: 0; + padding: 0; + border: 1px solid var(--line); + border-radius: 8px; + background: #f9fbfd; + color: #7c8798; + font-size: 19px; + line-height: 1; +} + +.icon-button:hover { + border-color: #b9c8e6; + color: #4a5568; +} + +.icon-button.danger:hover { + border-color: #efb8b8; + background: #fff5f5; + color: var(--danger); +} + +.config-path { + margin: 12px 22px 0; + padding: 9px 11px; + border: 1px solid #edf1f6; + border-radius: 8px; + background: #f8fafc; + display: flex; + align-items: center; + gap: 9px; + min-width: 0; + font-size: 11.5px; +} + +.config-path > span { + flex-shrink: 0; + color: #8d98a8; + font-weight: 700; +} + +.config-path code { + min-width: 0; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: #5d6b7f; + font-family: "SF Mono", Menlo, Consolas, monospace; +} + +.config-tabs { + padding: 12px 22px 0; + border-bottom: 1px solid var(--line); + display: flex; + gap: 4px; + overflow-x: auto; +} + +.config-tab { + flex-shrink: 0; + padding: 9px 11px; + border: none; + border-bottom: 2px solid transparent; + background: transparent; + color: #8d98a8; + font-size: 12.5px; + font-weight: 650; +} + +.config-tab:hover { + color: #4a5568; +} + +.config-tab.is-active { + border-bottom-color: var(--accent); + color: #2f3b4c; + font-weight: 750; +} + +.config-body { + min-height: 300px; + padding: 16px 22px; + overflow: auto; +} + +.editor-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 14px; + margin-bottom: 12px; +} + +.editor-head h3, +.extra-config h3 { + margin: 0; + font-size: 14.5px; + line-height: 1.35; +} + +.editor-head .hint { + margin-top: 3px; +} + +.table-wrap { + width: 100%; + overflow-x: auto; + border: 1px solid var(--line); + border-radius: 10px; +} + +.editor-table { + width: 100%; + border-collapse: collapse; + min-width: 430px; + background: #fff; +} + +.editor-table.wide-table { + min-width: 720px; +} + +.editor-table th { + padding: 8px 9px; + border-bottom: 1px solid var(--line); + background: #f8fafc; + color: #8d98a8; + text-align: left; + font-size: 11px; + font-weight: 750; + letter-spacing: 0.2px; + white-space: nowrap; +} + +.editor-table td { + padding: 6px 9px; + border-bottom: 1px solid #eef2f7; + vertical-align: middle; +} + +.editor-table tr:last-child td { + border-bottom: none; +} + +.editor-table input, +.editor-table select { + width: 100%; + min-width: 62px; + height: 34px; + padding: 0 9px; + border: 1px solid #dce4ef; + border-radius: 7px; + background: #fff; + color: #344054; + outline: none; + font-size: 12.5px; +} + +.editor-table input:focus, +.editor-table select:focus { + border-color: #7da0e8; + box-shadow: 0 0 0 2px rgba(53, 104, 212, 0.09); +} + +.editor-table input[type="checkbox"] { + width: 17px; + min-width: 17px; + height: 17px; + padding: 0; + accent-color: var(--blue); +} + +.check-col { + text-align: center; +} + +.action-col { + width: 44px; + text-align: center; +} + +.action-col .icon-button { + width: 27px; + height: 27px; + font-size: 16px; +} + +.sync-card { + padding: 16px; + border: 1px solid var(--line); + border-radius: 12px; + background: #fbfcfe; +} + +.sync-field { + display: block; + margin-top: 6px; +} + +.sync-field > span { + display: block; + margin-bottom: 6px; + color: #667085; + font-size: 12px; + font-weight: 700; +} + +.sync-input-row { + display: flex; + gap: 9px; +} + +.sync-input-row input { + flex: 1; + min-width: 0; + height: 40px; + padding: 0 12px; + border: 1px solid var(--line-strong); + border-radius: 9px; + background: #fff; + outline: none; + color: #344054; +} + +.sync-input-row input:focus { + border-color: #7da0e8; + box-shadow: 0 0 0 3px rgba(53, 104, 212, 0.09); +} + +.sync-status { + margin-top: 12px; + padding: 10px 12px; + border: 1px solid #dce8ff; + border-radius: 8px; + background: #f3f7ff; + color: #4e6da8; + font-size: 12px; + line-height: 1.55; +} + +.sync-status.is-error { + border-color: #f3c6c6; + background: #fff5f5; + color: #a83b3b; +} + +.sync-note { + margin-top: 12px; + color: #8d98a8; + font-size: 11.5px; + line-height: 1.7; +} + +.sync-note p { + margin: 0; +} + +.sync-note code, +.hint code { + padding: 1px 4px; + border-radius: 4px; + background: #eef2f7; + font-family: "SF Mono", Menlo, Consolas, monospace; + font-size: 10.5px; +} + +.extra-config { + margin-top: 16px; + padding-top: 14px; + border-top: 1px solid var(--line); +} + +.extra-config h3 { + margin-bottom: 10px; +} + +.extra-grid { + display: grid; + grid-template-columns: 1fr 1.4fr; + gap: 12px; +} + +.extra-grid label > span { + display: block; + margin-bottom: 6px; + color: #667085; + font-size: 12px; + font-weight: 700; +} + +.extra-grid input { + width: 100%; + height: 38px; + padding: 0 11px; + border: 1px solid var(--line-strong); + border-radius: 8px; + outline: none; + color: #344054; +} + +.extra-grid input:focus { + border-color: #7da0e8; + box-shadow: 0 0 0 3px rgba(53, 104, 212, 0.09); +} + +.modal-foot { + flex-shrink: 0; + padding: 13px 22px; + border-top: 1px solid var(--line); + background: #fcfdff; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.foot-left, +.foot-right { + display: flex; + align-items: center; + gap: 8px; +} + +.toast { + position: fixed; + left: 50%; + bottom: 26px; + z-index: 70; + max-width: min(680px, calc(100vw - 40px)); + padding: 10px 16px; + border-radius: 10px; + background: #243043; + color: #fff; + box-shadow: 0 12px 30px rgba(15, 23, 42, 0.25); + font-size: 12.5px; + line-height: 1.45; + transform: translateX(-50%); +} + +.toast[data-tone="error"] { + background: #a83b3b; +} + +@media (max-width: 900px) { + .layout { + grid-template-columns: 1fr; + overflow: auto; + } + + .left-panel { + overflow: visible; + } + + .right-panel { + min-height: 380px; + } + + .result-bar { + grid-template-columns: 1fr 1fr; + padding: 10px 0; + } + + .result-cell:nth-child(3) { + border-left: none; + } + + .result-cell:nth-child(n + 3) { + margin-top: 8px; + } +} diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..b4cdb60 --- /dev/null +++ b/frontend/vite.config.js @@ -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, + }, +}) diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts new file mode 100755 index 0000000..eca6f65 --- /dev/null +++ b/frontend/wailsjs/go/main/App.d.ts @@ -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; + +export function Calculate(arg1:pricing.Input):Promise; + +export function ExportConfig():Promise; + +export function ImportConfig():Promise; + +export function OpenConfigFolder():Promise; + +export function ResetConfig():Promise; + +export function SaveConfig(arg1:pricing.Config):Promise; + +export function SyncFromServer(arg1:string):Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js new file mode 100755 index 0000000..ac4a8eb --- /dev/null +++ b/frontend/wailsjs/go/main/App.js @@ -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); +} diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts new file mode 100755 index 0000000..fcbfa9d --- /dev/null +++ b/frontend/wailsjs/go/models.ts @@ -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; + daily_loss_m: number; + sale_ratio: number; + quantities: Record; + free_items: Record; + + 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; + } + } + +} + diff --git a/frontend/wailsjs/runtime/package.json b/frontend/wailsjs/runtime/package.json new file mode 100644 index 0000000..1e7c8a5 --- /dev/null +++ b/frontend/wailsjs/runtime/package.json @@ -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 ", + "license": "MIT", + "bugs": { + "url": "https://github.com/wailsapp/wails/issues" + }, + "homepage": "https://github.com/wailsapp/wails#readme" +} diff --git a/frontend/wailsjs/runtime/runtime.d.ts b/frontend/wailsjs/runtime/runtime.d.ts new file mode 100644 index 0000000..3bbea84 --- /dev/null +++ b/frontend/wailsjs/runtime/runtime.d.ts @@ -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; + +// [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; + +// [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; + +// [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; + +// [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; + +// [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; + +// [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; + +// [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; + +// [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; + +// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) +// Sets a text on the clipboard +export function ClipboardSetText(text: string): Promise; + +// [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; + +// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications) +// Cleans up notification resources and releases any held connections. +export function CleanupNotifications(): Promise; + +// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable) +// Checks if notifications are available on the current platform. +export function IsNotificationAvailable(): Promise; + +// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization) +// Requests notification authorization from the user (macOS only). +export function RequestNotificationAuthorization(): Promise; + +// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization) +// Checks the current notification authorization status (macOS only). +export function CheckNotificationAuthorization(): Promise; + +// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification) +// Sends a basic notification with the given options. +export function SendNotification(options: NotificationOptions): Promise; + +// [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; + +// [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; + +// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory) +// Removes a previously registered notification category. +export function RemoveNotificationCategory(categoryId: string): Promise; + +// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications) +// Removes all pending notifications from the notification center. +export function RemoveAllPendingNotifications(): Promise; + +// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification) +// Removes a specific pending notification by its identifier. +export function RemovePendingNotification(identifier: string): Promise; + +// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications) +// Removes all delivered notifications from the notification center. +export function RemoveAllDeliveredNotifications(): Promise; + +// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification) +// Removes a specific delivered notification by its identifier. +export function RemoveDeliveredNotification(identifier: string): Promise; + +// [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; \ No newline at end of file diff --git a/frontend/wailsjs/runtime/runtime.js b/frontend/wailsjs/runtime/runtime.js new file mode 100644 index 0000000..556621e --- /dev/null +++ b/frontend/wailsjs/runtime/runtime.js @@ -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); +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..068b678 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1e0a460 --- /dev/null +++ b/go.sum @@ -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= diff --git a/internal/pricing/calc.go b/internal/pricing/calc.go new file mode 100644 index 0000000..7b5805b --- /dev/null +++ b/internal/pricing/calc.go @@ -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 +} diff --git a/internal/pricing/calc_test.go b/internal/pricing/calc_test.go new file mode 100644 index 0000000..e140f55 --- /dev/null +++ b/internal/pricing/calc_test.go @@ -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 +} diff --git a/internal/pricing/config.go b/internal/pricing/config.go new file mode 100644 index 0000000..baf04b9 --- /dev/null +++ b/internal/pricing/config.go @@ -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) +} diff --git a/internal/pricing/defaults.go b/internal/pricing/defaults.go new file mode 100644 index 0000000..349ac9e --- /dev/null +++ b/internal/pricing/defaults.go @@ -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, + } +} diff --git a/internal/pricing/types.go b/internal/pricing/types.go new file mode 100644 index 0000000..56561ad --- /dev/null +++ b/internal/pricing/types.go @@ -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"` +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..d335e08 --- /dev/null +++ b/main.go @@ -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()) + } +} diff --git a/wails.json b/wails.json new file mode 100644 index 0000000..5accb1e --- /dev/null +++ b/wails.json @@ -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": "" + } +}