chore: init bj_power monorepo (dashboard/mes/wms/wms_client/workstation), clear subproject git metadata and align naming to folder names
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewClient(baseURL string) *Client {
|
||||
return &Client{
|
||||
baseURL: baseURL,
|
||||
client: &http.Client{},
|
||||
}
|
||||
}
|
||||
|
||||
type APIResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type WorkOrderInfo struct {
|
||||
WorkOrderNo string `json:"workOrderNo"`
|
||||
WorkpieceType string `json:"workpieceType"`
|
||||
WorkpieceTypeName string `json:"workpieceTypeName"`
|
||||
WorkpieceCount int `json:"workpieceCount"`
|
||||
Status string `json:"status"`
|
||||
StatusName string `json:"statusName"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
type MeasurementRecord struct {
|
||||
ID int `json:"id"`
|
||||
WorkOrderNo string `json:"workOrderNo"`
|
||||
WorkpieceNo string `json:"workpieceNo"`
|
||||
Category string `json:"category"`
|
||||
WorkpieceType string `json:"workpieceType"`
|
||||
ValueA float64 `json:"valueA"`
|
||||
ValueB float64 `json:"valueB"`
|
||||
ValueC float64 `json:"valueC"`
|
||||
Unit string `json:"unit"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
type MeasurementListResponse struct {
|
||||
List []MeasurementRecord `json:"list"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
type SubmitRequest struct {
|
||||
WorkOrderNo string `json:"workOrderNo"`
|
||||
WorkpieceNo string `json:"workpieceNo"`
|
||||
Category string `json:"category"`
|
||||
WorkpieceType string `json:"workpieceType"`
|
||||
ValueA float64 `json:"valueA"`
|
||||
ValueB *float64 `json:"valueB,omitempty"`
|
||||
ValueC *float64 `json:"valueC,omitempty"`
|
||||
Unit string `json:"unit"`
|
||||
Serial string `json:"serial"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
|
||||
type SubmitResponse struct {
|
||||
ID int `json:"id"`
|
||||
}
|
||||
|
||||
func (c *Client) QueryWorkOrder(ctx context.Context, workOrderNo string) (*WorkOrderInfo, error) {
|
||||
u, err := url.Parse(c.baseURL + "/api/v1/micrometer/work-order/query")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("workOrderNo", workOrderNo)
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var apiResp APIResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if apiResp.Code != 0 {
|
||||
return nil, fmt.Errorf("API error: %s", apiResp.Message)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(apiResp.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result WorkOrderInfo
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (c *Client) QueryMeasurements(ctx context.Context, workOrderNo, workpieceNo string, page, limit int) (*MeasurementListResponse, error) {
|
||||
u, err := url.Parse(c.baseURL + "/api/v1/micrometer/measurements")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("workOrderNo", workOrderNo)
|
||||
if workpieceNo != "" {
|
||||
q.Set("workpieceNo", workpieceNo)
|
||||
}
|
||||
q.Set("page", fmt.Sprintf("%d", page))
|
||||
q.Set("limit", fmt.Sprintf("%d", limit))
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var apiResp APIResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if apiResp.Code != 0 {
|
||||
return nil, fmt.Errorf("API error: %s", apiResp.Message)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(apiResp.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result MeasurementListResponse
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (c *Client) SubmitMeasurement(ctx context.Context, req SubmitRequest) (*SubmitResponse, error) {
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/api/v1/micrometer/measurements", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.client.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var apiResp APIResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if apiResp.Code != 0 {
|
||||
return nil, fmt.Errorf("API error: %s", apiResp.Message)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(apiResp.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result SubmitResponse
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (c *Client) QueryWorkOrderList(ctx context.Context, keyword string, limit int) ([]WorkOrderOption, error) {
|
||||
u, err := url.Parse(c.baseURL + "/api/v1/micrometer/work-orders")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q := u.Query()
|
||||
if keyword != "" {
|
||||
q.Set("keyword", keyword)
|
||||
}
|
||||
q.Set("limit", fmt.Sprintf("%d", limit))
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var apiResp APIResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if apiResp.Code != 0 {
|
||||
return nil, fmt.Errorf("API error: %s", apiResp.Message)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(apiResp.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result []WorkOrderOption
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type WorkOrderOption struct {
|
||||
WorkOrderNo string `json:"workOrderNo"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"bjhardman.cn/bjhardman/hougai_insize_micrometer/insize"
|
||||
)
|
||||
|
||||
type Unit int8
|
||||
|
||||
const (
|
||||
UnitMM Unit = 0
|
||||
UnitInch Unit = 1
|
||||
)
|
||||
|
||||
const MmPerInch = 25.4
|
||||
|
||||
type Collector struct {
|
||||
values []float64
|
||||
filled []bool
|
||||
current int
|
||||
serial uint32
|
||||
lastSeq byte
|
||||
itemDefs []string
|
||||
}
|
||||
|
||||
func NewCollector(itemDefs []string) *Collector {
|
||||
n := len(itemDefs)
|
||||
return &Collector{
|
||||
values: make([]float64, n),
|
||||
filled: make([]bool, n),
|
||||
current: 0,
|
||||
itemDefs: itemDefs,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Collector) ApplyFrame(f insize.Frame) {
|
||||
mm := f.Value
|
||||
if f.Unit == insize.UnitInch {
|
||||
mm = f.Value * MmPerInch
|
||||
}
|
||||
c.values[c.current] = mm
|
||||
c.filled[c.current] = true
|
||||
c.serial = f.SerialNo
|
||||
c.lastSeq = f.SeqNo
|
||||
if c.current < len(c.values)-1 {
|
||||
c.current++
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Collector) SetValue(i int, mm float64) {
|
||||
if i < 0 || i >= len(c.values) {
|
||||
return
|
||||
}
|
||||
c.values[i] = mm
|
||||
c.filled[i] = true
|
||||
c.current = i
|
||||
}
|
||||
|
||||
func (c *Collector) SetCurrent(i int) {
|
||||
if i >= 0 && i < len(c.values) {
|
||||
c.current = i
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Collector) CurrentIndex() int { return c.current }
|
||||
|
||||
func (c *Collector) DisplayValue(i int, u Unit) (string, bool) {
|
||||
if i < 0 || i >= len(c.values) || !c.filled[i] {
|
||||
return "----", false
|
||||
}
|
||||
mm := c.values[i]
|
||||
if u == UnitInch {
|
||||
return fmt.Sprintf("%.5f", mm/MmPerInch), true
|
||||
}
|
||||
return fmt.Sprintf("%.3f", mm), true
|
||||
}
|
||||
|
||||
func (c *Collector) AllFilled() bool {
|
||||
for _, ok := range c.filled {
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Collector) Clear() {
|
||||
for i := range c.values {
|
||||
c.values[i] = 0
|
||||
c.filled[i] = false
|
||||
}
|
||||
c.current = 0
|
||||
c.serial = 0
|
||||
c.lastSeq = 0
|
||||
}
|
||||
|
||||
func (c *Collector) Serial() (uint32, byte) {
|
||||
return c.serial, c.lastSeq
|
||||
}
|
||||
|
||||
func (c *Collector) Value(i int) float64 {
|
||||
if i < 0 || i >= len(c.values) {
|
||||
return 0
|
||||
}
|
||||
return c.values[i]
|
||||
}
|
||||
|
||||
func (c *Collector) Count() int {
|
||||
return len(c.values)
|
||||
}
|
||||
|
||||
func (c *Collector) ItemDefs() []string {
|
||||
return c.itemDefs
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//go:build !windows
|
||||
|
||||
package autostart
|
||||
|
||||
func CreateDesktopShortcut(appName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func Enable(appName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func Disable(appName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsEnabled(appName string) bool {
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package autostart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
func Enable(appName string) error {
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取可执行文件路径失败: %w", err)
|
||||
}
|
||||
exePath, err = filepath.Abs(exePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取绝对路径失败: %w", err)
|
||||
}
|
||||
|
||||
k, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Run`, registry.ALL_ACCESS)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开注册表失败: %w", err)
|
||||
}
|
||||
defer k.Close()
|
||||
|
||||
if err := k.SetStringValue(appName, exePath); err != nil {
|
||||
return fmt.Errorf("设置注册表值失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Disable(appName string) error {
|
||||
k, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Run`, registry.ALL_ACCESS)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开注册表失败: %w", err)
|
||||
}
|
||||
defer k.Close()
|
||||
|
||||
if err := k.DeleteValue(appName); err != nil {
|
||||
if err == registry.ErrNotExist {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("删除注册表值失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsEnabled(appName string) bool {
|
||||
k, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Run`, registry.READ)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer k.Close()
|
||||
|
||||
_, _, err = k.GetStringValue(appName)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func CreateDesktopShortcut(appName string) error {
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取exe路径失败: %w", err)
|
||||
}
|
||||
exePath, _ = filepath.Abs(exePath)
|
||||
|
||||
home, _ := os.UserHomeDir()
|
||||
shortcutPath := filepath.Join(home, "Desktop", appName+".lnk")
|
||||
|
||||
psCmd := fmt.Sprintf(
|
||||
`$ws = New-Object -ComObject WScript.Shell; $s = $ws.CreateShortcut('%s'); $s.TargetPath = '%s'; $s.WorkingDirectory = '%s'; $s.Save()`,
|
||||
shortcutPath, exePath, filepath.Dir(exePath))
|
||||
cmd := exec.Command("powershell", "-NoProfile", "-Command", psCmd)
|
||||
return cmd.Run()
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
APIURL string `json:"api_url"`
|
||||
SerialPort string `json:"serial_port"`
|
||||
BaudRate int `json:"baud_rate"`
|
||||
DisplayUnit string `json:"display_unit"`
|
||||
}
|
||||
|
||||
func Load(path string) (Config, error) {
|
||||
cfg := Config{
|
||||
APIURL: "http://localhost:8888",
|
||||
SerialPort: "COM7",
|
||||
BaudRate: 115200,
|
||||
DisplayUnit: "mm",
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if err := cfg.Save(path); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
return cfg, err
|
||||
}
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (c Config) Save(path string) error {
|
||||
data, err := json.MarshalIndent(c, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, 0644)
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package serialport
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"go.bug.st/serial"
|
||||
|
||||
"bjhardman.cn/bjhardman/hougai_insize_micrometer/insize"
|
||||
)
|
||||
|
||||
type Port struct {
|
||||
port serial.Port
|
||||
frames chan insize.Frame
|
||||
errs chan error
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func Open(name string, baud int) (*Port, error) {
|
||||
mode := &serial.Mode{
|
||||
BaudRate: baud,
|
||||
DataBits: 8,
|
||||
Parity: serial.NoParity,
|
||||
StopBits: serial.OneStopBit,
|
||||
}
|
||||
sp, err := serial.Open(name, mode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("打开串口 %s: %w", name, err)
|
||||
}
|
||||
if err := sp.SetReadTimeout(500 * time.Millisecond); err != nil {
|
||||
}
|
||||
|
||||
p := &Port{
|
||||
port: sp,
|
||||
frames: make(chan insize.Frame, 16),
|
||||
errs: make(chan error, 4),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
go p.readLoop()
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *Port) Frames() <-chan insize.Frame { return p.frames }
|
||||
|
||||
func (p *Port) Errors() <-chan error { return p.errs }
|
||||
|
||||
func (p *Port) Close() error {
|
||||
close(p.done)
|
||||
return p.port.Close()
|
||||
}
|
||||
|
||||
func (p *Port) readLoop() {
|
||||
reader := insize.NewReader(p.port)
|
||||
for {
|
||||
select {
|
||||
case <-p.done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
f, err := reader.ReadFrame()
|
||||
if err != nil {
|
||||
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
||||
p.errs <- fmt.Errorf("串口读取结束: %w", err)
|
||||
return
|
||||
}
|
||||
log.Printf("serialport: 跳过非法帧: %v", err)
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case p.frames <- f:
|
||||
case <-p.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,172 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ying32/govcl/vcl"
|
||||
"github.com/ying32/govcl/vcl/types"
|
||||
)
|
||||
|
||||
const (
|
||||
keyBtnSize int32 = 96
|
||||
keyFontSize int32 = 24
|
||||
dispFontSize int32 = 32
|
||||
gap int32 = 10
|
||||
margin int32 = 16
|
||||
)
|
||||
|
||||
// showNumKeyPad 弹出触屏数字键盘,返回(输入字符串, 是否确认)。
|
||||
// initial 为初始显示值;owner 通常为主窗体。
|
||||
// 布局:顶部显示框 + 左侧 4×3 数字网格 + 右侧纵向整列(退格/清空) + 底部整行(取消/确认)。
|
||||
func showNumKeyPad(owner vcl.IComponent, initial string) (string, bool) {
|
||||
form := vcl.NewForm(owner)
|
||||
form.SetCaption("输入数值")
|
||||
form.SetPosition(types.PoScreenCenter)
|
||||
form.SetBorderStyle(types.BsDialog)
|
||||
form.SetOnClose(func(sender vcl.IObject, action *types.TCloseAction) {
|
||||
*action = types.CaFree
|
||||
})
|
||||
|
||||
// 显示框高度随字号。
|
||||
dispH := dispFontSize + 24
|
||||
// 左侧 4 列 x 3 行数字网格。
|
||||
gridCols := int32(4)
|
||||
gridRows := int32(3)
|
||||
gridW := gridCols*keyBtnSize + (gridCols-1)*gap
|
||||
gridH := gridRows*keyBtnSize + (gridRows-1)*gap
|
||||
// 右侧功能键列宽与数字键同宽,纵向整列高度与网格同高,两个按钮均分。
|
||||
sideW := keyBtnSize
|
||||
sideH := gridH
|
||||
// 底部整行按钮高度与数字键同高。
|
||||
bottomH := keyBtnSize
|
||||
|
||||
formW := margin*2 + gridW + gap + sideW
|
||||
formH := margin + dispH + gap + gridH + gap + bottomH + margin
|
||||
form.SetWidth(formW)
|
||||
form.SetHeight(formH)
|
||||
|
||||
// 显示框:横跨整个宽度。
|
||||
display := vcl.NewEdit(form)
|
||||
display.SetParent(form)
|
||||
display.SetLeft(margin)
|
||||
display.SetTop(margin)
|
||||
display.SetWidth(formW - margin*2)
|
||||
display.SetHeight(dispH)
|
||||
display.Font().SetSize(dispFontSize)
|
||||
display.SetText(initial)
|
||||
display.SetReadOnly(true)
|
||||
display.SetAlignment(types.TaRightJustify)
|
||||
|
||||
var (
|
||||
result string
|
||||
confirmed bool
|
||||
)
|
||||
|
||||
curText := func() string {
|
||||
var s string
|
||||
display.GetTextBuf(&s, 64)
|
||||
return s
|
||||
}
|
||||
setText := func(s string) { display.SetText(s) }
|
||||
|
||||
makeBtn := func(caption string, l, t, w, h int32) *vcl.TButton {
|
||||
b := vcl.NewButton(form)
|
||||
b.SetParent(form)
|
||||
b.SetLeft(l)
|
||||
b.SetTop(t)
|
||||
b.SetWidth(w)
|
||||
b.SetHeight(h)
|
||||
b.SetCaption(caption)
|
||||
b.Font().SetSize(keyFontSize)
|
||||
return b
|
||||
}
|
||||
|
||||
gridTop := margin + dispH + gap
|
||||
gridLeft := margin
|
||||
|
||||
// 数字键 4 列 x 3 行:
|
||||
// [7][8][9][.]
|
||||
// [4][5][6][0]
|
||||
// [1][2][3][+/-]
|
||||
layout := []string{"7", "8", "9", ".", "4", "5", "6", "0", "1", "2", "3", "+/-"}
|
||||
for i, cap := range layout {
|
||||
col := int32(i % int(gridCols))
|
||||
row := int32(i / int(gridCols))
|
||||
btn := makeBtn(cap,
|
||||
gridLeft+col*(keyBtnSize+gap),
|
||||
gridTop+row*(keyBtnSize+gap),
|
||||
keyBtnSize, keyBtnSize)
|
||||
btn.SetOnClick(func(sender vcl.IObject) {
|
||||
c := vcl.AsButton(sender).Caption()
|
||||
s := curText()
|
||||
switch c {
|
||||
case "+/-":
|
||||
if strings.HasPrefix(s, "-") {
|
||||
s = s[1:]
|
||||
} else if s != "" {
|
||||
s = "-" + s
|
||||
}
|
||||
case ".":
|
||||
if !strings.Contains(s, ".") {
|
||||
if s == "" || s == "-" {
|
||||
s += "0."
|
||||
} else {
|
||||
s += "."
|
||||
}
|
||||
}
|
||||
default:
|
||||
s += c
|
||||
}
|
||||
setText(s)
|
||||
})
|
||||
}
|
||||
|
||||
// 右侧纵向整列:退格 / 清空,均分高度。
|
||||
sideLeft := margin + gridW + gap
|
||||
sideBtnH := (sideH - gap) / 2 // 两按钮间留一个 gap
|
||||
sideBtns := []struct {
|
||||
cap string
|
||||
fn func()
|
||||
}{
|
||||
{"退格", func() {
|
||||
s := curText()
|
||||
if len(s) > 0 {
|
||||
s = s[:len(s)-1]
|
||||
}
|
||||
setText(s)
|
||||
}},
|
||||
{"清空", func() { setText("") }},
|
||||
}
|
||||
for i, b := range sideBtns {
|
||||
btn := makeBtn(b.cap, sideLeft, gridTop+int32(i)*(sideBtnH+gap), sideW, sideBtnH)
|
||||
btn.SetOnClick(func(sender vcl.IObject) { b.fn() })
|
||||
}
|
||||
|
||||
// 底部整行:取消 / 确认,均分宽度。
|
||||
bottomTop := gridTop + gridH + gap
|
||||
bottomW := (formW - margin*2 - gap) / 2 // 两按钮间留一个 gap
|
||||
cancel := makeBtn("取消", margin, bottomTop, bottomW, bottomH)
|
||||
cancel.SetOnClick(func(sender vcl.IObject) {
|
||||
confirmed = false
|
||||
form.Close()
|
||||
})
|
||||
ok := makeBtn("确认", margin+bottomW+gap, bottomTop, bottomW, bottomH)
|
||||
ok.Font().SetSize(keyFontSize + 4)
|
||||
ok.SetOnClick(func(sender vcl.IObject) {
|
||||
s := curText()
|
||||
if s == "" || s == "-" {
|
||||
confirmed = false
|
||||
} else if _, err := strconv.ParseFloat(s, 64); err != nil {
|
||||
vcl.ShowMessage("请输入合法数值")
|
||||
return
|
||||
} else {
|
||||
result = s
|
||||
confirmed = true
|
||||
}
|
||||
form.Close()
|
||||
})
|
||||
|
||||
form.ShowModal()
|
||||
return result, confirmed
|
||||
}
|
||||
Reference in New Issue
Block a user